Merge changes from topic "am-88276146-0d74-467c-8b81-8d471d84ce66" into oc-dev am: 2c28ae3933 am: d142b28c2b am: a5860449c1
am: 3c46516f9e

Change-Id: I814a8a8b52084dcbcfefa5960a827a3d43fb7f54
diff --git a/.clang-format b/.clang-format
index 0e0f4d1..4a3bd2f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,3 +1,33 @@
+# bootable/recovery project uses repohook to apply `clang-format` to the changed lines, with the
+# local style file in `.clang-format`. This will be triggered automatically with `repo upload`.
+# Alternatively, one can stage and format a change with `git clang-format` directly.
+#
+#   $ git add <files>
+#   $ git clang-format --style file
+#
+# Or to format a committed change.
+#
+#   $ git clang-format --style file HEAD~1
+#
+# `--style file` will pick up the local style file in `.clang-format`. This can be configured as the
+# default behavior for bootable/recovery project.
+#
+#   $ git config --local clangFormat.style file
+#
+# Note that `repo upload` calls the `clang-format` binary in Android repo (i.e.
+# `$ANDROID_BUILD_TOP/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format`), which might
+# give slightly different results from the one installed in host machine (e.g.
+# `/usr/bin/clang-format`). Specifying the file with `--binary` will ensure consistent results.
+#
+#  $ git clang-format --binary \
+#      /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+# Or to do one-time setup to make it default.
+#
+#   $ git config --local clangFormat.binary \
+#       /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+
 BasedOnStyle: Google
 AllowShortBlocksOnASingleLine: false
 AllowShortFunctionsOnASingleLine: Empty
diff --git a/Android.bp b/Android.bp
index f8c6a4b..76e6985 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,8 +1,304 @@
-subdirs = [
-    "applypatch",
-    "bootloader_message",
-    "edify",
-    "otafault",
-    "otautil",
-    "uncrypt",
-]
+// 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.
+
+cc_defaults {
+    name: "recovery_defaults",
+
+    cflags: [
+        "-D_FILE_OFFSET_BITS=64",
+
+        // Must be the same as RECOVERY_API_VERSION.
+        "-DRECOVERY_API_VERSION=3",
+
+        "-Wall",
+        "-Werror",
+    ],
+
+    cpp_std: "c++17",
+}
+
+cc_library {
+    name: "librecovery_ui",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "device.cpp",
+        "screen_ui.cpp",
+        "ui.cpp",
+        "vr_ui.cpp",
+        "wear_ui.cpp"
+    ],
+
+    static_libs: [
+        "libminui",
+        "libotautil",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libpng",
+        "libz",
+    ],
+}
+
+// Generic device that uses ScreenRecoveryUI.
+cc_library_static {
+    name: "librecovery_ui_default",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "default_device.cpp",
+    ],
+}
+
+// The default wear device that uses WearRecoveryUI.
+cc_library_static {
+    name: "librecovery_ui_wear",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "wear_device.cpp",
+    ],
+}
+
+// The default VR device that uses VrRecoveryUI.
+cc_library_static {
+    name: "librecovery_ui_vr",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "vr_device.cpp",
+    ],
+}
+
+cc_library_static {
+    name: "librecovery_fastboot",
+    recovery_available: true,
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "fastboot/fastboot.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libbootloader_message",
+        "libcutils",
+        "liblog",
+    ],
+
+    static_libs: [
+        "librecovery_ui_default",
+    ],
+}
+
+cc_defaults {
+    name: "librecovery_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    shared_libs: [
+        "android.hardware.health@2.0",
+        "libbase",
+        "libbootloader_message",
+        "libcrypto",
+        "libcutils",
+        "libext4_utils",
+        "libfs_mgr",
+        "libfusesideload",
+        "libhidl-gen-utils",
+        "libhidlbase",
+        "libhidltransport",
+        "liblog",
+        "libpng",
+        "libselinux",
+        "libtinyxml2",
+        "libutils",
+        "libz",
+        "libziparchive",
+    ],
+
+    static_libs: [
+        "librecovery_fastboot",
+        "libminui",
+        "libverifier",
+        "libotautil",
+
+        // external dependencies
+        "libhealthhalutils",
+        "libvintf_recovery",
+        "libvintf",
+    ],
+}
+
+cc_library_static {
+    name: "librecovery",
+    recovery_available: true,
+
+    defaults: [
+        "librecovery_defaults",
+    ],
+
+    srcs: [
+        "adb_install.cpp",
+        "fsck_unshare_blocks.cpp",
+        "fuse_sdcard_provider.cpp",
+        "install.cpp",
+        "recovery.cpp",
+        "roots.cpp",
+    ],
+
+    include_dirs: [
+        "system/vold",
+    ],
+}
+
+cc_library_static {
+    name: "libverifier",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "asn1_decoder.cpp",
+        "verifier.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+        "libcrypto_utils",
+    ],
+
+    static_libs: [
+        "libotautil",
+    ],
+}
+
+cc_binary {
+    name: "recovery",
+    recovery: true,
+
+    defaults: [
+        "librecovery_defaults",
+    ],
+
+    srcs: [
+        "logging.cpp",
+        "recovery_main.cpp",
+    ],
+
+    shared_libs: [
+        "libminadbd_services",
+        "librecovery_ui",
+    ],
+
+    static_libs: [
+        "librecovery",
+        "librecovery_ui_default",
+    ],
+
+    required: [
+        "e2fsdroid.recovery",
+        "librecovery_ui_ext",
+        "mke2fs.conf.recovery",
+        "mke2fs.recovery",
+        "recovery_deps",
+    ],
+}
+
+// The dynamic executable that runs after /data mounts.
+cc_binary {
+    name: "recovery-persist",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "logging.cpp",
+        "recovery-persist.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+        "libmetricslogger",
+    ],
+
+    static_libs: [
+        "libotautil",
+    ],
+
+    init_rc: [
+        "recovery-persist.rc",
+    ],
+}
+
+// The dynamic executable that runs at init.
+cc_binary {
+    name: "recovery-refresh",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    srcs: [
+        "logging.cpp",
+        "recovery-refresh.cpp",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "liblog",
+    ],
+
+    static_libs: [
+        "libotautil",
+    ],
+
+    init_rc: [
+        "recovery-refresh.rc",
+    ],
+}
+
+filegroup {
+    name: "res-testdata",
+
+    srcs: [
+        "res-*/images/*_text.png",
+    ],
+}
diff --git a/Android.mk b/Android.mk
index 7e0ad12..7be1230 100644
--- a/Android.mk
+++ b/Android.mk
@@ -14,267 +14,73 @@
 
 LOCAL_PATH := $(call my-dir)
 
-# Needed by build/make/core/Makefile.
+# Needed by build/make/core/Makefile. Must be consistent with the value in Android.bp.
 RECOVERY_API_VERSION := 3
 RECOVERY_FSTAB_VERSION := 2
 
-# libfusesideload (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := fuse_sideload.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_CFLAGS += -D_XOPEN_SOURCE -D_GNU_SOURCE
-LOCAL_MODULE := libfusesideload
-LOCAL_STATIC_LIBRARIES := \
-    libcrypto \
-    libbase
-include $(BUILD_STATIC_LIBRARY)
+# TARGET_RECOVERY_UI_LIB should be one of librecovery_ui_{default,wear,vr} or a device-specific
+# module that defines make_device() and the exact RecoveryUI class for the target. It defaults to
+# librecovery_ui_default, which uses ScreenRecoveryUI.
+TARGET_RECOVERY_UI_LIB ?= librecovery_ui_default
 
-# libmounts (static library)
-# ===============================
+# librecovery_ui_ext (shared library)
+# ===================================
 include $(CLEAR_VARS)
-LOCAL_SRC_FILES := mounts.cpp
-LOCAL_CFLAGS := \
-    -Wall \
-    -Werror
-LOCAL_MODULE := libmounts
-LOCAL_STATIC_LIBRARIES := libbase
-include $(BUILD_STATIC_LIBRARY)
 
-# librecovery (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    install.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
+LOCAL_MODULE := librecovery_ui_ext
 
-ifeq ($(AB_OTA_UPDATER),true)
-    LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
+# LOCAL_MODULE_PATH for shared libraries is unsupported in multiarch builds.
+LOCAL_MULTILIB := first
+
+ifeq ($(TARGET_IS_64_BIT),true)
+LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/system/lib64
+else
+LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/system/lib
 endif
 
-LOCAL_MODULE := librecovery
-LOCAL_STATIC_LIBRARIES := \
-    libminui \
-    libotautil \
-    libvintf_recovery \
-    libcrypto_utils \
-    libcrypto \
+LOCAL_WHOLE_STATIC_LIBRARIES := \
+    $(TARGET_RECOVERY_UI_LIB)
+
+LOCAL_SHARED_LIBRARIES := \
     libbase \
-    libziparchive \
+    liblog \
+    librecovery_ui.recovery
 
-include $(BUILD_STATIC_LIBRARY)
+include $(BUILD_SHARED_LIBRARY)
 
-# recovery (static executable)
-# ===============================
+# recovery_deps: A phony target that's depended on by `recovery`, which
+# builds additional modules conditionally based on Makefile variables.
+# ======================================================================
 include $(CLEAR_VARS)
 
-LOCAL_SRC_FILES := \
-    adb_install.cpp \
-    device.cpp \
-    fuse_sdcard_provider.cpp \
-    recovery.cpp \
-    roots.cpp \
-    rotate_logs.cpp \
-    screen_ui.cpp \
-    ui.cpp \
-    vr_ui.cpp \
-    wear_ui.cpp \
-
-LOCAL_MODULE := recovery
-
-LOCAL_FORCE_STATIC_EXECUTABLE := true
-
-LOCAL_REQUIRED_MODULES := e2fsdroid_static mke2fs_static mke2fs.conf
+LOCAL_MODULE := recovery_deps
 
 ifeq ($(TARGET_USERIMAGES_USE_F2FS),true)
 ifeq ($(HOST_OS),linux)
-LOCAL_REQUIRED_MODULES += sload.f2fs mkfs.f2fs
+LOCAL_REQUIRED_MODULES += \
+    sload.f2fs \
+    mkfs.f2fs
 endif
 endif
 
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
-LOCAL_CFLAGS += -Wall -Werror
-
-ifneq ($(TARGET_RECOVERY_UI_MARGIN_HEIGHT),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=$(TARGET_RECOVERY_UI_MARGIN_HEIGHT)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=0
+# e2fsck is needed for adb remount -R.
+ifeq ($(BOARD_EXT4_SHARE_DUP_BLOCKS),true)
+ifneq (,$(filter userdebug eng,$(TARGET_BUILD_VARIANT)))
+LOCAL_REQUIRED_MODULES += \
+    e2fsck_static
+endif
 endif
 
-ifneq ($(TARGET_RECOVERY_UI_MARGIN_WIDTH),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_WIDTH=$(TARGET_RECOVERY_UI_MARGIN_WIDTH)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_WIDTH=0
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_TOUCH_LOW_THRESHOLD),)
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_LOW_THRESHOLD=$(TARGET_RECOVERY_UI_TOUCH_LOW_THRESHOLD)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_LOW_THRESHOLD=50
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_TOUCH_HIGH_THRESHOLD),)
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_HIGH_THRESHOLD=$(TARGET_RECOVERY_UI_TOUCH_HIGH_THRESHOLD)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_HIGH_THRESHOLD=90
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_PROGRESS_BAR_BASELINE),)
-LOCAL_CFLAGS += -DRECOVERY_UI_PROGRESS_BAR_BASELINE=$(TARGET_RECOVERY_UI_PROGRESS_BAR_BASELINE)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_PROGRESS_BAR_BASELINE=259
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_ANIMATION_FPS),)
-LOCAL_CFLAGS += -DRECOVERY_UI_ANIMATION_FPS=$(TARGET_RECOVERY_UI_ANIMATION_FPS)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_ANIMATION_FPS=30
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_MENU_UNUSABLE_ROWS),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MENU_UNUSABLE_ROWS=$(TARGET_RECOVERY_UI_MENU_UNUSABLE_ROWS)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MENU_UNUSABLE_ROWS=9
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_VR_STEREO_OFFSET),)
-LOCAL_CFLAGS += -DRECOVERY_UI_VR_STEREO_OFFSET=$(TARGET_RECOVERY_UI_VR_STEREO_OFFSET)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_VR_STEREO_OFFSET=0
-endif
-
-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 \
-    libbootloader_message \
-    libfs_mgr \
-    libext4_utils \
-    libsparse \
-    libziparchive \
-    libotautil \
-    libmounts \
-    libminadbd \
-    libasyncio \
-    libfusesideload \
-    libminui \
-    libpng \
-    libcrypto_utils \
-    libcrypto \
-    libvintf_recovery \
-    libvintf \
-    libhidl-gen-utils \
-    libtinyxml2 \
-    libbase \
-    libutils \
-    libcutils \
-    liblog \
-    libselinux \
-    libz
-
-LOCAL_HAL_STATIC_LIBRARIES := libhealthd
-
-ifeq ($(AB_OTA_UPDATER),true)
-    LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
-endif
-
-LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/sbin
-
-ifeq ($(TARGET_RECOVERY_UI_LIB),)
-  LOCAL_SRC_FILES += default_device.cpp
-else
-  LOCAL_STATIC_LIBRARIES += $(TARGET_RECOVERY_UI_LIB)
-endif
-
+# On A/B devices recovery-persist reads the recovery related file from the persist storage and
+# copies them into /data/misc/recovery. Then, for both A/B and non-A/B devices, recovery-persist
+# parses the last_install file and reports the embedded update metrics. Also, the last_install file
+# will be deteleted after the report.
+LOCAL_REQUIRED_MODULES += recovery-persist
 ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),)
-LOCAL_REQUIRED_MODULES += recovery-persist recovery-refresh
+LOCAL_REQUIRED_MODULES += recovery-refresh
 endif
 
-include $(BUILD_EXECUTABLE)
-
-# recovery-persist (system partition dynamic executable run after /data mounts)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    recovery-persist.cpp \
-    rotate_logs.cpp
-LOCAL_MODULE := recovery-persist
-LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_INIT_RC := recovery-persist.rc
-include $(BUILD_EXECUTABLE)
-
-# recovery-refresh (system partition dynamic executable run at init)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
-    recovery-refresh.cpp \
-    rotate_logs.cpp
-LOCAL_MODULE := recovery-refresh
-LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_INIT_RC := recovery-refresh.rc
-include $(BUILD_EXECUTABLE)
-
-# libverifier (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_MODULE := libverifier
-LOCAL_SRC_FILES := \
-    asn1_decoder.cpp \
-    verifier.cpp
-LOCAL_STATIC_LIBRARIES := \
-    libotautil \
-    libcrypto_utils \
-    libcrypto \
-    libbase
-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
-
-include $(BUILD_STATIC_LIBRARY)
-
-# vr headset default device
-# ===============================
-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
-
-include $(BUILD_STATIC_LIBRARY)
+include $(BUILD_PHONY_PACKAGE)
 
 include \
-    $(LOCAL_PATH)/boot_control/Android.mk \
-    $(LOCAL_PATH)/minadbd/Android.mk \
-    $(LOCAL_PATH)/minui/Android.mk \
-    $(LOCAL_PATH)/tests/Android.mk \
-    $(LOCAL_PATH)/tools/Android.mk \
     $(LOCAL_PATH)/updater/Android.mk \
-    $(LOCAL_PATH)/update_verifier/Android.mk \
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index b5f5f03..1084291 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -4,3 +4,8 @@
 [Builtin Hooks Options]
 # Handle native codes only.
 clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+                  -fw updater_sample/
+
diff --git a/adb_install.cpp b/adb_install.cpp
index ac01306..438da9f 100644
--- a/adb_install.cpp
+++ b/adb_install.cpp
@@ -26,55 +26,23 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
-#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/properties.h>
-#include <android-base/unique_fd.h>
 
 #include "common.h"
 #include "fuse_sideload.h"
 #include "install.h"
 #include "ui.h"
 
-static void set_usb_driver(bool enabled) {
-  // USB configfs doesn't use /s/c/a/a/enable.
-  if (android::base::GetBoolProperty("sys.usb.configfs", false)) {
-    return;
-  }
-
-  static constexpr const char* USB_DRIVER_CONTROL = "/sys/class/android_usb/android0/enable";
-  android::base::unique_fd fd(open(USB_DRIVER_CONTROL, O_WRONLY));
-  if (fd == -1) {
-    PLOG(ERROR) << "Failed to open driver control";
-    return;
-  }
-  // Not using android::base::WriteStringToFile since that will open with O_CREAT and give EPERM
-  // when USB_DRIVER_CONTROL doesn't exist. When it gives EPERM, we don't know whether that's due
-  // to non-existent USB_DRIVER_CONTROL or indeed a permission issue.
-  if (!android::base::WriteStringToFd(enabled ? "1" : "0", fd)) {
-    PLOG(ERROR) << "Failed to set driver control";
-  }
-}
-
-static void stop_adbd() {
-  ui->Print("Stopping adbd...\n");
-  android::base::SetProperty("ctl.stop", "adbd");
-  set_usb_driver(false);
-}
-
-static void maybe_restart_adbd() {
-  if (is_ro_debuggable()) {
-    ui->Print("Restarting adbd...\n");
-    set_usb_driver(true);
-    android::base::SetProperty("ctl.start", "adbd");
-  }
-}
-
-int apply_from_adb(bool* wipe_cache, const char* install_file) {
+int apply_from_adb(bool* wipe_cache) {
   modified_flash = true;
-
-  stop_adbd();
-  set_usb_driver(true);
+  // Save the usb state to restore after the sideload operation.
+  std::string usb_state = android::base::GetProperty("sys.usb.state", "none");
+  // Clean up state and stop adbd.
+  if (usb_state != "none" && !SetUsbConfig("none")) {
+    LOG(ERROR) << "Failed to clear USB config";
+    return INSTALL_ERROR;
+  }
 
   ui->Print(
       "\n\nNow send the package you want to apply\n"
@@ -82,10 +50,15 @@
 
   pid_t child;
   if ((child = fork()) == 0) {
-    execl("/sbin/recovery", "recovery", "--adbd", nullptr);
+    execl("/system/bin/recovery", "recovery", "--adbd", nullptr);
     _exit(EXIT_FAILURE);
   }
 
+  if (!SetUsbConfig("sideload")) {
+    LOG(ERROR) << "Failed to set usb config to sideload";
+    return INSTALL_ERROR;
+  }
+
   // How long (in seconds) we wait for the host to start sending us a package, before timing out.
   static constexpr int ADB_INSTALL_TIMEOUT = 300;
 
@@ -113,7 +86,7 @@
         break;
       }
     }
-    result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, install_file, false, 0);
+    result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0);
     break;
   }
 
@@ -136,8 +109,17 @@
     }
   }
 
-  set_usb_driver(false);
-  maybe_restart_adbd();
+  // Clean up before switching to the older state, for example setting the state
+  // to none sets sys/class/android_usb/android0/enable to 0.
+  if (!SetUsbConfig("none")) {
+    LOG(ERROR) << "Failed to clear USB config";
+  }
+
+  if (usb_state != "none") {
+    if (!SetUsbConfig(usb_state)) {
+      LOG(ERROR) << "Failed to set USB config to " << usb_state;
+    }
+  }
 
   return result;
 }
diff --git a/adb_install.h b/adb_install.h
index e654c89..cc3ca26 100644
--- a/adb_install.h
+++ b/adb_install.h
@@ -17,6 +17,6 @@
 #ifndef _ADB_INSTALL_H
 #define _ADB_INSTALL_H
 
-int apply_from_adb(bool* wipe_cache, const char* install_file);
+int apply_from_adb(bool* wipe_cache);
 
 #endif
diff --git a/applypatch/Android.bp b/applypatch/Android.bp
index cb0b367..620ca6c 100644
--- a/applypatch/Android.bp
+++ b/applypatch/Android.bp
@@ -53,7 +53,6 @@
         "libbz",
         "libcrypto",
         "libedify",
-        "libotafault",
         "libotautil",
         "libz",
     ],
@@ -100,7 +99,6 @@
         "libapplypatch_modes",
         "libapplypatch",
         "libedify",
-        "libotafault",
         "libotautil",
         "libbspatch",
     ],
diff --git a/applypatch/applypatch.cpp b/applypatch/applypatch.cpp
index 7645a40..f9383dd 100644
--- a/applypatch/applypatch.cpp
+++ b/applypatch/applypatch.cpp
@@ -23,261 +23,167 @@
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
-#include <sys/statfs.h>
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <algorithm>
 #include <functional>
 #include <memory>
 #include <string>
 #include <utility>
 #include <vector>
 
+#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/strings.h>
+#include <android-base/unique_fd.h>
 #include <openssl/sha.h>
 
 #include "edify/expr.h"
-#include "otafault/ota_io.h"
-#include "otautil/cache_location.h"
+#include "otautil/paths.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);
-static int GenerateTarget(const FileContents& source_file, const std::unique_ptr<Value>& patch,
-                          const std::string& target_filename,
-                          const uint8_t target_sha1[SHA_DIGEST_LENGTH], const Value* bonus_data);
+using namespace std::string_literals;
 
-// Read a file into memory; store the file contents and associated metadata in *file.
-// Return 0 on success.
-int LoadFileContents(const char* filename, FileContents* file) {
-  // A special 'filename' beginning with "EMMC:" means to load the contents of a partition.
-  if (strncmp(filename, "EMMC:", 5) == 0) {
-    return LoadPartitionContents(filename, file);
+static bool GenerateTarget(const Partition& target, const FileContents& source_file,
+                           const Value& patch, const Value* bonus_data);
+
+bool LoadFileContents(const std::string& filename, FileContents* file) {
+  // No longer allow loading contents from eMMC partitions.
+  if (android::base::StartsWith(filename, "EMMC:")) {
+    return false;
   }
 
-  struct stat sb;
-  if (stat(filename, &sb) == -1) {
-    printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
-    return -1;
+  std::string data;
+  if (!android::base::ReadFileToString(filename, &data)) {
+    PLOG(ERROR) << "Failed to read \"" << filename << "\"";
+    return false;
   }
 
-  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));
-    return -1;
-  }
-
-  size_t bytes_read = ota_fread(data.data(), 1, data.size(), f.get());
-  if (bytes_read != data.size()) {
-    printf("short read of \"%s\" (%zu bytes of %zu)\n", filename, bytes_read, data.size());
-    return -1;
-  }
-  file->data = std::move(data);
+  file->data = std::vector<unsigned char>(data.begin(), data.end());
   SHA1(file->data.data(), file->data.size(), file->sha1);
-  return 0;
+  return true;
 }
 
-// Load the contents of an EMMC partition into the provided
-// FileContents.  filename should be a string of the form
-// "EMMC:<partition_device>:...".  The smallest size_n bytes for
-// which that prefix of the partition contents has the corresponding
-// sha1 hash will be loaded.  It is acceptable for a size value to be
-// repeated with different sha1s.  Will return 0 on success.
-//
-// This complexity is needed because if an OTA installation is
-// interrupted, the partition might contain either the source or the
-// target data, which might be of different lengths.  We need to know
-// the length in order to read from a partition (there is no
-// "end-of-file" marker), so the caller must specify the possible
-// lengths and the hash of the data, and we'll do the load expecting
-// to find one of those hashes.
-static int LoadPartitionContents(const std::string& filename, FileContents* file) {
-  std::vector<std::string> pieces = android::base::Split(filename, ":");
-  if (pieces.size() < 4 || pieces.size() % 2 != 0 || pieces[0] != "EMMC") {
-    printf("LoadPartitionContents called with bad filename \"%s\"\n", filename.c_str());
-    return -1;
+// Reads the contents of a Partition to the given FileContents buffer.
+static bool ReadPartitionToBuffer(const Partition& partition, FileContents* out,
+                                  bool check_backup) {
+  uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+  if (ParseSha1(partition.hash, expected_sha1) != 0) {
+    LOG(ERROR) << "Failed to parse target hash \"" << partition.hash << "\"";
+    return false;
   }
 
-  size_t pair_count = (pieces.size() - 2) / 2;  // # of (size, sha1) pairs in filename
-  std::vector<std::pair<size_t, std::string>> pairs;
-  for (size_t i = 0; i < pair_count; ++i) {
-    size_t size;
-    if (!android::base::ParseUint(pieces[i * 2 + 2], &size) || size == 0) {
-      printf("LoadPartitionContents called with bad size \"%s\"\n", pieces[i * 2 + 2].c_str());
-      return -1;
-    }
-    pairs.push_back({ size, pieces[i * 2 + 3] });
-  }
-
-  // Sort the pairs array so that they are in order of increasing size.
-  std::sort(pairs.begin(), pairs.end());
-
-  const char* partition = pieces[1].c_str();
-  unique_file dev(ota_fopen(partition, "rb"));
+  android::base::unique_fd dev(open(partition.name.c_str(), O_RDONLY));
   if (!dev) {
-    printf("failed to open emmc partition \"%s\": %s\n", partition, strerror(errno));
-    return -1;
-  }
-
-  SHA_CTX sha_ctx;
-  SHA1_Init(&sha_ctx);
-
-  // Allocate enough memory to hold the largest size.
-  std::vector<unsigned char> buffer(pairs[pair_count - 1].first);
-  unsigned char* buffer_ptr = buffer.data();
-  size_t buffer_size = 0;  // # bytes read so far
-  bool found = false;
-
-  for (const auto& pair : pairs) {
-    size_t current_size = pair.first;
-    const std::string& current_sha1 = pair.second;
-
-    // Read enough additional bytes to get us up to the next size. (Again,
-    // we're trying the possibilities in order of increasing size).
-    size_t next = current_size - buffer_size;
-    if (next > 0) {
-      size_t read = ota_fread(buffer_ptr, 1, next, dev.get());
-      if (next != read) {
-        printf("short read (%zu bytes of %zu) for partition \"%s\"\n", read, next, partition);
-        return -1;
+    PLOG(ERROR) << "Failed to open eMMC partition \"" << partition << "\"";
+  } else {
+    std::vector<unsigned char> buffer(partition.size);
+    if (!android::base::ReadFully(dev, buffer.data(), buffer.size())) {
+      PLOG(ERROR) << "Failed to read " << buffer.size() << " bytes of data for partition "
+                  << partition;
+    } else {
+      SHA1(buffer.data(), buffer.size(), out->sha1);
+      if (memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) {
+        out->data = std::move(buffer);
+        return true;
       }
-      SHA1_Update(&sha_ctx, buffer_ptr, read);
-      buffer_size += read;
-      buffer_ptr += read;
-    }
-
-    // Duplicate the SHA context and finalize the duplicate so we can
-    // check it against this pair's expected hash.
-    SHA_CTX temp_ctx;
-    memcpy(&temp_ctx, &sha_ctx, sizeof(SHA_CTX));
-    uint8_t sha_so_far[SHA_DIGEST_LENGTH];
-    SHA1_Final(sha_so_far, &temp_ctx);
-
-    uint8_t parsed_sha[SHA_DIGEST_LENGTH];
-    if (ParseSha1(current_sha1.c_str(), parsed_sha) != 0) {
-      printf("failed to parse SHA-1 %s in %s\n", current_sha1.c_str(), filename.c_str());
-      return -1;
-    }
-
-    if (memcmp(sha_so_far, parsed_sha, SHA_DIGEST_LENGTH) == 0) {
-      // We have a match. Stop reading the partition; we'll return the data we've read so far.
-      printf("partition read matched size %zu SHA-1 %s\n", current_size, current_sha1.c_str());
-      found = true;
-      break;
     }
   }
 
-  if (!found) {
-    // Ran off the end of the list of (size, sha1) pairs without finding a match.
-    printf("contents of partition \"%s\" didn't match %s\n", partition, filename.c_str());
-    return -1;
+  if (!check_backup) {
+    LOG(ERROR) << "Partition contents don't have the expected checksum";
+    return false;
   }
 
-  SHA1_Final(file->sha1, &sha_ctx);
+  if (LoadFileContents(Paths::Get().cache_temp_source(), out) &&
+      memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) {
+    return true;
+  }
 
-  buffer.resize(buffer_size);
-  file->data = std::move(buffer);
-
-  return 0;
+  LOG(ERROR) << "Both of partition contents and backup don't have the expected checksum";
+  return false;
 }
 
-// Save the contents of the given FileContents object under the given
-// filename.  Return 0 on success.
-int SaveFileContents(const char* filename, const FileContents* file) {
-  unique_fd fd(ota_open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR));
+bool SaveFileContents(const std::string& filename, const FileContents* file) {
+  android::base::unique_fd fd(
+      open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR));
   if (fd == -1) {
-    printf("failed to open \"%s\" for write: %s\n", filename, strerror(errno));
-    return -1;
+    PLOG(ERROR) << "Failed to open \"" << filename << "\" for write";
+    return false;
   }
 
-  size_t bytes_written = FileSink(file->data.data(), file->data.size(), fd);
-  if (bytes_written != file->data.size()) {
-    printf("short write of \"%s\" (%zd bytes of %zu): %s\n", filename, bytes_written,
-           file->data.size(), strerror(errno));
-    return -1;
-  }
-  if (ota_fsync(fd) != 0) {
-    printf("fsync of \"%s\" failed: %s\n", filename, strerror(errno));
-    return -1;
-  }
-  if (ota_close(fd) != 0) {
-    printf("close of \"%s\" failed: %s\n", filename, strerror(errno));
-    return -1;
+  if (!android::base::WriteFully(fd, file->data.data(), file->data.size())) {
+    PLOG(ERROR) << "Failed to write " << file->data.size() << " bytes of data to " << filename;
+    return false;
   }
 
-  return 0;
+  if (fsync(fd) != 0) {
+    PLOG(ERROR) << "Failed to fsync \"" << filename << "\"";
+    return false;
+  }
+
+  if (close(fd.release()) != 0) {
+    PLOG(ERROR) << "Failed to close \"" << filename << "\"";
+    return false;
+  }
+
+  return true;
 }
 
-// Write a memory buffer to 'target' partition, a string of the form
-// "EMMC:<partition_device>[:...]". The target name
-// might contain multiple colons, but WriteToPartition() only uses the first
-// two and ignores the rest. Return 0 on success.
-int WriteToPartition(const unsigned char* data, size_t len, const std::string& target) {
-  std::vector<std::string> pieces = android::base::Split(target, ":");
-  if (pieces.size() < 2 || pieces[0] != "EMMC") {
-    printf("WriteToPartition called with bad target (%s)\n", target.c_str());
-    return -1;
-  }
-
-  const char* partition = pieces[1].c_str();
-  unique_fd fd(ota_open(partition, O_RDWR));
-  if (fd == -1) {
-    printf("failed to open %s: %s\n", partition, strerror(errno));
-    return -1;
-  }
-
+// Writes a memory buffer to 'target' Partition.
+static bool WriteBufferToPartition(const FileContents& file_contents, const Partition& partition) {
+  const unsigned char* data = file_contents.data.data();
+  size_t len = file_contents.data.size();
   size_t start = 0;
   bool success = false;
   for (size_t attempt = 0; attempt < 2; ++attempt) {
-    if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) {
-      printf("failed seek on %s: %s\n", partition, strerror(errno));
-      return -1;
-    }
-    while (start < len) {
-      size_t to_write = len - start;
-      if (to_write > 1 << 20) to_write = 1 << 20;
-
-      ssize_t written = TEMP_FAILURE_RETRY(ota_write(fd, data + start, to_write));
-      if (written == -1) {
-        printf("failed write writing to %s: %s\n", partition, strerror(errno));
-        return -1;
-      }
-      start += written;
-    }
-
-    if (ota_fsync(fd) != 0) {
-      printf("failed to sync to %s: %s\n", partition, strerror(errno));
-      return -1;
-    }
-    if (ota_close(fd) != 0) {
-      printf("failed to close %s: %s\n", partition, strerror(errno));
-      return -1;
-    }
-
-    fd.reset(ota_open(partition, O_RDONLY));
+    android::base::unique_fd fd(open(partition.name.c_str(), O_RDWR));
     if (fd == -1) {
-      printf("failed to reopen %s for verify: %s\n", partition, strerror(errno));
-      return -1;
+      PLOG(ERROR) << "Failed to open \"" << partition << "\"";
+      return false;
+    }
+
+    if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) {
+      PLOG(ERROR) << "Failed to seek to " << start << " on \"" << partition << "\"";
+      return false;
+    }
+
+    if (!android::base::WriteFully(fd, data + start, len - start)) {
+      PLOG(ERROR) << "Failed to write " << len - start << " bytes to \"" << partition << "\"";
+      return false;
+    }
+
+    if (fsync(fd) != 0) {
+      PLOG(ERROR) << "Failed to sync \"" << partition << "\"";
+      return false;
+    }
+    if (close(fd.release()) != 0) {
+      PLOG(ERROR) << "Failed to close \"" << partition << "\"";
+      return false;
+    }
+
+    fd.reset(open(partition.name.c_str(), O_RDONLY));
+    if (fd == -1) {
+      PLOG(ERROR) << "Failed to reopen \"" << partition << "\" for verification";
+      return false;
     }
 
     // Drop caches so our subsequent verification read won't just be reading the cache.
     sync();
-    unique_fd dc(ota_open("/proc/sys/vm/drop_caches", O_WRONLY));
-    if (TEMP_FAILURE_RETRY(ota_write(dc, "3\n", 2)) == -1) {
-      printf("write to /proc/sys/vm/drop_caches failed: %s\n", strerror(errno));
+    std::string drop_cache = "/proc/sys/vm/drop_caches";
+    if (!android::base::WriteStringToFile("3\n", drop_cache)) {
+      PLOG(ERROR) << "Failed to write to " << drop_cache;
     } else {
-      printf("  caches dropped\n");
+      LOG(INFO) << "  caches dropped";
     }
-    ota_close(dc);
     sleep(1);
 
     // Verify.
     if (TEMP_FAILURE_RETRY(lseek(fd, 0, SEEK_SET)) == -1) {
-      printf("failed to seek back to beginning of %s: %s\n", partition, strerror(errno));
-      return -1;
+      PLOG(ERROR) << "Failed to seek to 0 on " << partition;
+      return false;
     }
 
     unsigned char buffer[4096];
@@ -288,386 +194,262 @@
         to_read = sizeof(buffer);
       }
 
-      size_t so_far = 0;
-      while (so_far < to_read) {
-        ssize_t read_count = TEMP_FAILURE_RETRY(ota_read(fd, buffer + so_far, to_read - so_far));
-        if (read_count == -1) {
-          printf("verify read error %s at %zu: %s\n", partition, p, strerror(errno));
-          return -1;
-        } else if (read_count == 0) {
-          printf("verify read reached unexpected EOF, %s at %zu\n", partition, p);
-          return -1;
-        }
-        if (static_cast<size_t>(read_count) < to_read) {
-          printf("short verify read %s at %zu: %zd %zu\n", partition, p, read_count, to_read);
-        }
-        so_far += read_count;
+      if (!android::base::ReadFully(fd, buffer, to_read)) {
+        PLOG(ERROR) << "Failed to verify-read " << partition << " at " << p;
+        return false;
       }
 
       if (memcmp(buffer, data + p, to_read) != 0) {
-        printf("verification failed starting at %zu\n", p);
+        LOG(ERROR) << "Verification failed starting at " << p;
         start = p;
         break;
       }
     }
 
     if (start == len) {
-      printf("verification read succeeded (attempt %zu)\n", attempt + 1);
+      LOG(INFO) << "Verification read succeeded (attempt " << attempt + 1 << ")";
       success = true;
       break;
     }
 
-    if (ota_close(fd) != 0) {
-      printf("failed to close %s: %s\n", partition, strerror(errno));
-      return -1;
-    }
-
-    fd.reset(ota_open(partition, O_RDWR));
-    if (fd == -1) {
-      printf("failed to reopen %s for retry write && verify: %s\n", partition, strerror(errno));
-      return -1;
+    if (close(fd.release()) != 0) {
+      PLOG(ERROR) << "Failed to close " << partition;
+      return false;
     }
   }
 
   if (!success) {
-    printf("failed to verify after all attempts\n");
-    return -1;
+    LOG(ERROR) << "Failed to verify after all attempts";
+    return false;
   }
 
-  if (ota_close(fd) == -1) {
-    printf("error closing %s: %s\n", partition, strerror(errno));
-    return -1;
-  }
   sync();
 
+  return true;
+}
+
+int ParseSha1(const std::string& str, uint8_t* digest) {
+  const char* ps = str.c_str();
+  uint8_t* pd = digest;
+  for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) {
+    int digit;
+    if (*ps >= '0' && *ps <= '9') {
+      digit = *ps - '0';
+    } else if (*ps >= 'a' && *ps <= 'f') {
+      digit = *ps - 'a' + 10;
+    } else if (*ps >= 'A' && *ps <= 'F') {
+      digit = *ps - 'A' + 10;
+    } else {
+      return -1;
+    }
+    if (i % 2 == 0) {
+      *pd = digit << 4;
+    } else {
+      *pd |= digit;
+      ++pd;
+    }
+  }
+  if (*ps != '\0') return -1;
   return 0;
 }
 
-// Take a string 'str' of 40 hex digits and parse it into the 20
-// byte array 'digest'.  'str' may contain only the digest or be of
-// the form "<digest>:<anything>".  Return 0 on success, -1 on any
-// error.
-int ParseSha1(const char* str, uint8_t* digest) {
-    const char* ps = str;
-    uint8_t* pd = digest;
-    for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) {
-        int digit;
-        if (*ps >= '0' && *ps <= '9') {
-            digit = *ps - '0';
-        } else if (*ps >= 'a' && *ps <= 'f') {
-            digit = *ps - 'a' + 10;
-        } else if (*ps >= 'A' && *ps <= 'F') {
-            digit = *ps - 'A' + 10;
-        } else {
-            return -1;
-        }
-        if (i % 2 == 0) {
-            *pd = digit << 4;
-        } else {
-            *pd |= digit;
-            ++pd;
-        }
-    }
-    if (*ps != '\0') return -1;
-    return 0;
-}
-
-// Search an array of sha1 strings for one matching the given sha1.
-// Return the index of the match on success, or -1 if no match is
-// found.
-static int FindMatchingPatch(uint8_t* sha1, const std::vector<std::string>& patch_sha1_str) {
-  for (size_t i = 0; i < patch_sha1_str.size(); ++i) {
-    uint8_t patch_sha1[SHA_DIGEST_LENGTH];
-    if (ParseSha1(patch_sha1_str[i].c_str(), patch_sha1) == 0 &&
-        memcmp(patch_sha1, sha1, SHA_DIGEST_LENGTH) == 0) {
-      return i;
-    }
-  }
-  return -1;
-}
-
-// Returns 0 if the contents of the file (argv[2]) or the cached file
-// match any of the sha1's on the command line (argv[3:]).  Returns
-// nonzero otherwise.
-int applypatch_check(const char* filename, const std::vector<std::string>& patch_sha1_str) {
-  FileContents file;
-
-  // It's okay to specify no sha1s; the check will pass if the
-  // LoadFileContents is successful.  (Useful for reading
-  // partitions, where the filename encodes the sha1s; no need to
-  // check them twice.)
-  if (LoadFileContents(filename, &file) != 0 ||
-      (!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(CacheLocation::location().cache_temp_source().c_str(), &file) != 0) {
-      printf("failed to load cache file\n");
-      return 1;
-    }
-
-    if (FindMatchingPatch(file.sha1, patch_sha1_str) < 0) {
-      printf("cache bits don't match any sha1 for \"%s\"\n", filename);
-      return 1;
-    }
-  }
-  return 0;
+bool PatchPartitionCheck(const Partition& target, const Partition& source) {
+  FileContents target_file;
+  FileContents source_file;
+  return (ReadPartitionToBuffer(target, &target_file, false) ||
+          ReadPartitionToBuffer(source, &source_file, true));
 }
 
 int ShowLicenses() {
-    ShowBSDiffLicense();
-    return 0;
-}
-
-static size_t FileSink(const unsigned char* data, size_t len, int fd) {
-  size_t done = 0;
-  while (done < len) {
-    ssize_t wrote = TEMP_FAILURE_RETRY(ota_write(fd, data + done, len - done));
-    if (wrote == -1) {
-      printf("error writing %zd bytes: %s\n", (len - done), strerror(errno));
-      return done;
-    }
-    done += wrote;
-  }
-  return done;
-}
-
-// Return the amount of free space (in bytes) on the filesystem
-// containing filename.  filename must exist.  Return -1 on error.
-size_t FreeSpaceForFile(const char* filename) {
-    struct statfs sf;
-    if (statfs(filename, &sf) != 0) {
-        printf("failed to statfs %s: %s\n", filename, strerror(errno));
-        return -1;
-    }
-    return sf.f_bsize * sf.f_bavail;
-}
-
-int CacheSizeCheck(size_t bytes) {
-    if (MakeFreeSpaceOnCache(bytes) < 0) {
-        printf("unable to make %zu bytes available on /cache\n", bytes);
-        return 1;
-    }
-    return 0;
-}
-
-// This function applies binary patches to EMMC target files in a way that is safe (the original
-// file is not touched until we have the desired replacement for it) and idempotent (it's okay to
-// run this program multiple times).
-//
-// - If the SHA-1 hash of <target_filename> is <target_sha1_string>, does nothing and exits
-//   successfully.
-//
-// - Otherwise, if the SHA-1 hash of <source_filename> is one of the entries in <patch_sha1_str>,
-//   the corresponding patch from <patch_data> (which must be a VAL_BLOB) is applied to produce a
-//   new file (the type of patch is automatically detected from the blob data). If that new file
-//   has SHA-1 hash <target_sha1_str>, moves it to replace <target_filename>, and exits
-//   successfully. Note that if <source_filename> and <target_filename> are not the same,
-//   <source_filename> is NOT deleted on success. <target_filename> may be the string "-" to mean
-//   "the same as <source_filename>".
-//
-// - Otherwise, or if any error is encountered, exits with non-zero status.
-//
-// <source_filename> must refer to an EMMC partition to read the source data. See the comments for
-// the LoadPartitionContents() function above for the format of such a filename. <target_size> has
-// 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 */,
-               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);
-
-  if (target_filename[0] == '-' && target_filename[1] == '\0') {
-    target_filename = source_filename;
-  }
-
-  if (strncmp(target_filename, "EMMC:", 5) != 0) {
-    printf("Supporting patching EMMC targets only.\n");
-    return 1;
-  }
-
-  uint8_t target_sha1[SHA_DIGEST_LENGTH];
-  if (ParseSha1(target_sha1_str, target_sha1) != 0) {
-    printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
-    return 1;
-  }
-
-  // We try to load the target file into the source_file object.
-  FileContents source_file;
-  if (LoadFileContents(target_filename, &source_file) == 0) {
-    if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
-      // The early-exit case: the patch was already applied, this file has the desired hash, nothing
-      // for us to do.
-      printf("already %s\n", short_sha1(target_sha1).c_str());
-      return 0;
-    }
-  }
-
-  if (source_file.data.empty() ||
-      (target_filename != source_filename && strcmp(target_filename, source_filename) != 0)) {
-    // Need to load the source file: either we failed to load the target file, or we did but it's
-    // different from the expected.
-    source_file.data.clear();
-    LoadFileContents(source_filename, &source_file);
-  }
-
-  if (!source_file.data.empty()) {
-    int to_use = FindMatchingPatch(source_file.sha1, patch_sha1_str);
-    if (to_use != -1) {
-      return GenerateTarget(source_file, patch_data[to_use], target_filename, target_sha1,
-                            bonus_data);
-    }
-  }
-
-  printf("source file is bad; trying copy\n");
-
-  FileContents copy_file;
-  if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), &copy_file) < 0) {
-    printf("failed to read copy file\n");
-    return 1;
-  }
-
-  int to_use = FindMatchingPatch(copy_file.sha1, patch_sha1_str);
-  if (to_use == -1) {
-    printf("copy file doesn't match source SHA-1s either\n");
-    return 1;
-  }
-
-  return GenerateTarget(copy_file, patch_data[to_use], target_filename, target_sha1, bonus_data);
-}
-
-/*
- * This function flashes a given image to the target partition. It verifies
- * the target cheksum first, and will return if target has the desired hash.
- * It checks the checksum of the given source image before flashing, and
- * verifies the target partition afterwards. The function is idempotent.
- * Returns zero on success.
- */
-int applypatch_flash(const char* source_filename, const char* target_filename,
-                     const char* target_sha1_str, size_t target_size) {
-  printf("flash %s: ", target_filename);
-
-  uint8_t target_sha1[SHA_DIGEST_LENGTH];
-  if (ParseSha1(target_sha1_str, target_sha1) != 0) {
-    printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
-    return 1;
-  }
-
-  std::string target_str(target_filename);
-  std::vector<std::string> pieces = android::base::Split(target_str, ":");
-  if (pieces.size() != 2 || pieces[0] != "EMMC") {
-    printf("invalid target name \"%s\"", target_filename);
-    return 1;
-  }
-
-  // Load the target into the source_file object to see if already applied.
-  pieces.push_back(std::to_string(target_size));
-  pieces.push_back(target_sha1_str);
-  std::string fullname = android::base::Join(pieces, ':');
-  FileContents source_file;
-  if (LoadPartitionContents(fullname, &source_file) == 0 &&
-      memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
-    // The early-exit case: the image was already applied, this partition
-    // has the desired hash, nothing for us to do.
-    printf("already %s\n", short_sha1(target_sha1).c_str());
-    return 0;
-  }
-
-  if (LoadFileContents(source_filename, &source_file) == 0) {
-    if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
-      // The source doesn't have desired checksum.
-      printf("source \"%s\" doesn't have expected sha1 sum\n", source_filename);
-      printf("expected: %s, found: %s\n", short_sha1(target_sha1).c_str(),
-             short_sha1(source_file.sha1).c_str());
-      return 1;
-    }
-  }
-
-  if (WriteToPartition(source_file.data.data(), target_size, target_filename) != 0) {
-    printf("write of copied data to %s failed\n", target_filename);
-    return 1;
-  }
+  ShowBSDiffLicense();
   return 0;
 }
 
-static int GenerateTarget(const FileContents& source_file, const std::unique_ptr<Value>& patch,
-                          const std::string& target_filename,
-                          const uint8_t target_sha1[SHA_DIGEST_LENGTH], const Value* bonus_data) {
-  if (patch->type != VAL_BLOB) {
-    printf("patch is not a blob\n");
-    return 1;
+bool PatchPartition(const Partition& target, const Partition& source, const Value& patch,
+                    const Value* bonus) {
+  LOG(INFO) << "Patching " << target.name;
+
+  // We try to load and check against the target hash first.
+  FileContents target_file;
+  if (ReadPartitionToBuffer(target, &target_file, false)) {
+    // The early-exit case: the patch was already applied, this file has the desired hash, nothing
+    // for us to do.
+    LOG(INFO) << "  already " << target.hash.substr(0, 8);
+    return true;
   }
 
-  const char* header = &patch->data[0];
-  size_t header_bytes_read = patch->data.size();
+  FileContents source_file;
+  if (ReadPartitionToBuffer(source, &source_file, true)) {
+    return GenerateTarget(target, source_file, patch, bonus);
+  }
+
+  LOG(ERROR) << "Failed to find any match";
+  return false;
+}
+
+bool FlashPartition(const Partition& partition, const std::string& source_filename) {
+  LOG(INFO) << "Flashing " << partition;
+
+  // We try to load and check against the target hash first.
+  FileContents target_file;
+  if (ReadPartitionToBuffer(partition, &target_file, false)) {
+    // The early-exit case: the patch was already applied, this file has the desired hash, nothing
+    // for us to do.
+    LOG(INFO) << "  already " << partition.hash.substr(0, 8);
+    return true;
+  }
+
+  FileContents source_file;
+  if (!LoadFileContents(source_filename, &source_file)) {
+    LOG(ERROR) << "Failed to load source file";
+    return false;
+  }
+
+  uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+  if (ParseSha1(partition.hash, expected_sha1) != 0) {
+    LOG(ERROR) << "Failed to parse source hash \"" << partition.hash << "\"";
+    return false;
+  }
+
+  if (memcmp(source_file.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) {
+    // The source doesn't have desired checksum.
+    LOG(ERROR) << "source \"" << source_filename << "\" doesn't have expected SHA-1 sum";
+    LOG(ERROR) << "expected: " << partition.hash.substr(0, 8)
+               << ", found: " << short_sha1(source_file.sha1);
+    return false;
+  }
+  if (!WriteBufferToPartition(source_file, partition)) {
+    LOG(ERROR) << "Failed to write to " << partition;
+    return false;
+  }
+  return true;
+}
+
+static bool GenerateTarget(const Partition& target, const FileContents& source_file,
+                           const Value& patch, const Value* bonus_data) {
+  uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+  if (ParseSha1(target.hash, expected_sha1) != 0) {
+    LOG(ERROR) << "Failed to parse target hash \"" << target.hash << "\"";
+    return false;
+  }
+
+  if (patch.type != Value::Type::BLOB) {
+    LOG(ERROR) << "patch is not a blob";
+    return false;
+  }
+
+  const char* header = patch.data.data();
+  size_t header_bytes_read = patch.data.size();
   bool use_bsdiff = false;
   if (header_bytes_read >= 8 && memcmp(header, "BSDIFF40", 8) == 0) {
     use_bsdiff = true;
   } else if (header_bytes_read >= 8 && memcmp(header, "IMGDIFF2", 8) == 0) {
     use_bsdiff = false;
   } else {
-    printf("Unknown patch file format\n");
-    return 1;
+    LOG(ERROR) << "Unknown patch file format";
+    return false;
   }
 
-  CHECK(android::base::StartsWith(target_filename, "EMMC:"));
-
-  // We still write the original source to cache, in case the partition write is interrupted.
-  if (MakeFreeSpaceOnCache(source_file.data.size()) < 0) {
-    printf("not enough free space on /cache\n");
-    return 1;
+  // We write the original source to cache, in case the partition write is interrupted.
+  if (!CheckAndFreeSpaceOnCache(source_file.data.size())) {
+    LOG(ERROR) << "Not enough free space on /cache";
+    return false;
   }
-  if (SaveFileContents(CacheLocation::location().cache_temp_source().c_str(), &source_file) < 0) {
-    printf("failed to back up source file\n");
-    return 1;
+  if (!SaveFileContents(Paths::Get().cache_temp_source(), &source_file)) {
+    LOG(ERROR) << "Failed to back up source file";
+    return false;
   }
 
   // We store the decoded output in memory.
-  std::string memory_sink_str;  // Don't need to reserve space.
-  SinkFn sink = [&memory_sink_str](const unsigned char* data, size_t len) {
-    memory_sink_str.append(reinterpret_cast<const char*>(data), len);
+  FileContents patched;
+  SHA_CTX ctx;
+  SHA1_Init(&ctx);
+  SinkFn sink = [&patched, &ctx](const unsigned char* data, size_t len) {
+    SHA1_Update(&ctx, data, len);
+    patched.data.insert(patched.data.end(), data, data + len);
     return len;
   };
 
-  SHA_CTX ctx;
-  SHA1_Init(&ctx);
-
   int result;
   if (use_bsdiff) {
-    result =
-        ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), *patch, 0, sink, &ctx);
+    result = ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), patch, 0, sink);
   } else {
-    result = ApplyImagePatch(source_file.data.data(), source_file.data.size(), *patch, sink, &ctx,
-                             bonus_data);
+    result =
+        ApplyImagePatch(source_file.data.data(), source_file.data.size(), patch, sink, bonus_data);
   }
 
   if (result != 0) {
-    printf("applying patch failed\n");
-    return 1;
+    LOG(ERROR) << "Failed to apply the patch: " << result;
+    return false;
   }
 
-  uint8_t current_target_sha1[SHA_DIGEST_LENGTH];
-  SHA1_Final(current_target_sha1, &ctx);
-  if (memcmp(current_target_sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
-    printf("patch did not produce expected sha1\n");
-    return 1;
-  } else {
-    printf("now %s\n", short_sha1(target_sha1).c_str());
+  SHA1_Final(patched.sha1, &ctx);
+  if (memcmp(patched.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) {
+    LOG(ERROR) << "Patching did not produce the expected SHA-1 of " << short_sha1(expected_sha1);
+
+    LOG(ERROR) << "target size " << patched.data.size() << " SHA-1 " << short_sha1(patched.sha1);
+    LOG(ERROR) << "source size " << source_file.data.size() << " SHA-1 "
+               << short_sha1(source_file.sha1);
+
+    uint8_t patch_digest[SHA_DIGEST_LENGTH];
+    SHA1(reinterpret_cast<const uint8_t*>(patch.data.data()), patch.data.size(), patch_digest);
+    LOG(ERROR) << "patch size " << patch.data.size() << " SHA-1 " << short_sha1(patch_digest);
+
+    if (bonus_data != nullptr) {
+      uint8_t bonus_digest[SHA_DIGEST_LENGTH];
+      SHA1(reinterpret_cast<const uint8_t*>(bonus_data->data.data()), bonus_data->data.size(),
+           bonus_digest);
+      LOG(ERROR) << "bonus size " << bonus_data->data.size() << " SHA-1 "
+                 << short_sha1(bonus_digest);
+    }
+
+    return false;
   }
 
+  LOG(INFO) << "  now " << short_sha1(expected_sha1);
+
   // Write back the temp file to the partition.
-  if (WriteToPartition(reinterpret_cast<const unsigned char*>(memory_sink_str.c_str()),
-                       memory_sink_str.size(), target_filename) != 0) {
-    printf("write of patched data to %s failed\n", target_filename.c_str());
-    return 1;
+  if (!WriteBufferToPartition(patched, target)) {
+    LOG(ERROR) << "Failed to write patched data to " << target.name;
+    return false;
   }
 
   // Delete the backup copy of the source.
-  unlink(CacheLocation::location().cache_temp_source().c_str());
+  unlink(Paths::Get().cache_temp_source().c_str());
 
   // Success!
-  return 0;
+  return true;
+}
+
+bool CheckPartition(const Partition& partition) {
+  FileContents target_file;
+  return ReadPartitionToBuffer(partition, &target_file, false);
+}
+
+Partition Partition::Parse(const std::string& input_str, std::string* err) {
+  std::vector<std::string> pieces = android::base::Split(input_str, ":");
+  if (pieces.size() != 4 || pieces[0] != "EMMC") {
+    *err = "Invalid number of tokens or non-eMMC target";
+    return {};
+  }
+
+  size_t size;
+  if (!android::base::ParseUint(pieces[2], &size) || size == 0) {
+    *err = "Failed to parse \"" + pieces[2] + "\" as byte count";
+    return {};
+  }
+
+  return Partition(pieces[1], size, pieces[3]);
+}
+
+std::string Partition::ToString() const {
+  if (*this) {
+    return "EMMC:"s + name + ":" + std::to_string(size) + ":" + hash;
+  }
+  return "<invalid-partition>";
+}
+
+std::ostream& operator<<(std::ostream& os, const Partition& partition) {
+  os << partition.ToString();
+  return os;
 }
diff --git a/applypatch/applypatch_main.cpp b/applypatch/applypatch_main.cpp
index 197077c..92d2b3f 100644
--- a/applypatch/applypatch_main.cpp
+++ b/applypatch/applypatch_main.cpp
@@ -16,13 +16,10 @@
 
 #include "applypatch_modes.h"
 
-// This program (applypatch) applies binary patches to files in a way that
-// is safe (the original file is not touched until we have the desired
-// replacement for it) and idempotent (it's okay to run this program
-// multiple times).
-//
-// See the comments to applypatch_modes() function.
+#include <android-base/logging.h>
 
+// See the comments for applypatch() function.
 int main(int argc, char** argv) {
-    return applypatch_modes(argc, const_cast<const char**>(argv));
+  android::base::InitLogging(argv);
+  return applypatch_modes(argc, argv);
 }
diff --git a/applypatch/applypatch_modes.cpp b/applypatch/applypatch_modes.cpp
index aa32d57..b466598 100644
--- a/applypatch/applypatch_modes.cpp
+++ b/applypatch/applypatch_modes.cpp
@@ -16,6 +16,7 @@
 
 #include "applypatch_modes.h"
 
+#include <getopt.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
@@ -25,6 +26,8 @@
 #include <string>
 #include <vector>
 
+#include <android-base/file.h>
+#include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/strings.h>
 #include <openssl/sha.h>
@@ -32,157 +35,153 @@
 #include "applypatch/applypatch.h"
 #include "edify/expr.h"
 
-static int CheckMode(int argc, const char** argv) {
-    if (argc < 3) {
+static int CheckMode(const std::string& target_emmc) {
+  std::string err;
+  auto target = Partition::Parse(target_emmc, &err);
+  if (!target) {
+    LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+    return 2;
+  }
+  return CheckPartition(target) ? 0 : 1;
+}
+
+static int FlashMode(const std::string& target_emmc, const std::string& source_file) {
+  std::string err;
+  auto target = Partition::Parse(target_emmc, &err);
+  if (!target) {
+    LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+    return 2;
+  }
+  return FlashPartition(target, source_file) ? 0 : 1;
+}
+
+static int PatchMode(const std::string& target_emmc, const std::string& source_emmc,
+                     const std::string& patch_file, const std::string& bonus_file) {
+  std::string err;
+  auto target = Partition::Parse(target_emmc, &err);
+  if (!target) {
+    LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+    return 2;
+  }
+
+  auto source = Partition::Parse(source_emmc, &err);
+  if (!source) {
+    LOG(ERROR) << "Failed to parse source \"" << source_emmc << "\": " << err;
+    return 2;
+  }
+
+  std::string patch_contents;
+  if (!android::base::ReadFileToString(patch_file, &patch_contents)) {
+    PLOG(ERROR) << "Failed to read patch file \"" << patch_file << "\"";
+    return 1;
+  }
+
+  Value patch(Value::Type::BLOB, std::move(patch_contents));
+  std::unique_ptr<Value> bonus;
+  if (!bonus_file.empty()) {
+    std::string bonus_contents;
+    if (!android::base::ReadFileToString(bonus_file, &bonus_contents)) {
+      PLOG(ERROR) << "Failed to read bonus file \"" << bonus_file << "\"";
+      return 1;
+    }
+    bonus = std::make_unique<Value>(Value::Type::BLOB, std::move(bonus_contents));
+  }
+
+  return PatchPartition(target, source, patch, bonus.get()) ? 0 : 1;
+}
+
+static void Usage() {
+  printf(
+      "Usage: \n"
+      "check mode\n"
+      "  applypatch --check EMMC:<target-file>:<target-size>:<target-sha1>\n\n"
+      "flash mode\n"
+      "  applypatch --flash <source-file>\n"
+      "             --target EMMC:<target-file>:<target-size>:<target-sha1>\n\n"
+      "patch mode\n"
+      "  applypatch [--bonus <bonus-file>]\n"
+      "             --patch <patch-file>\n"
+      "             --target EMMC:<target-file>:<target-size>:<target-sha1>\n"
+      "             --source EMMC:<source-file>:<source-size>:<source-sha1>\n\n"
+      "show license\n"
+      "  applypatch --license\n"
+      "\n\n");
+}
+
+int applypatch_modes(int argc, char* argv[]) {
+  static constexpr struct option OPTIONS[]{
+    // clang-format off
+    { "bonus", required_argument, nullptr, 0 },
+    { "check", required_argument, nullptr, 0 },
+    { "flash", required_argument, nullptr, 0 },
+    { "license", no_argument, nullptr, 0 },
+    { "patch", required_argument, nullptr, 0 },
+    { "source", required_argument, nullptr, 0 },
+    { "target", required_argument, nullptr, 0 },
+    { nullptr, 0, nullptr, 0 },
+    // clang-format on
+  };
+
+  std::string check_target;
+  std::string source;
+  std::string target;
+  std::string patch;
+  std::string bonus;
+
+  bool check_mode = false;
+  bool flash_mode = false;
+  bool patch_mode = false;
+
+  optind = 1;
+
+  int arg;
+  int option_index;
+  while ((arg = getopt_long(argc, argv, "", OPTIONS, &option_index)) != -1) {
+    switch (arg) {
+      case 0: {
+        std::string option = OPTIONS[option_index].name;
+        if (option == "bonus") {
+          bonus = optarg;
+        } else if (option == "check") {
+          check_target = optarg;
+          check_mode = true;
+        } else if (option == "flash") {
+          source = optarg;
+          flash_mode = true;
+        } else if (option == "license") {
+          return ShowLicenses();
+        } else if (option == "patch") {
+          patch = optarg;
+          patch_mode = true;
+        } else if (option == "source") {
+          source = optarg;
+        } else if (option == "target") {
+          target = optarg;
+        }
+        break;
+      }
+      case '?':
+      default:
+        LOG(ERROR) << "Invalid argument";
+        Usage();
         return 2;
     }
-    std::vector<std::string> sha1;
-    for (int i = 3; i < argc; i++) {
-        sha1.push_back(argv[i]);
+  }
+
+  if (check_mode) {
+    return CheckMode(check_target);
+  }
+  if (flash_mode) {
+    if (!bonus.empty()) {
+      LOG(ERROR) << "bonus file not supported in flash mode";
+      return 1;
     }
+    return FlashMode(target, source);
+  }
+  if (patch_mode) {
+    return PatchMode(target, source, patch, bonus);
+  }
 
-    return applypatch_check(argv[2], sha1);
-}
-
-// Parse arguments (which should be of the form "<sha1>:<filename>" into the
-// new parallel arrays *sha1s and *files. Returns true on success.
-static bool ParsePatchArgs(int argc, const char** argv, std::vector<std::string>* sha1s,
-                           std::vector<FileContents>* files) {
-    if (sha1s == nullptr) {
-        return false;
-    }
-    for (int i = 0; i < argc; ++i) {
-        std::vector<std::string> pieces = android::base::Split(argv[i], ":");
-        if (pieces.size() != 2) {
-            printf("failed to parse patch argument \"%s\"\n", argv[i]);
-            return false;
-        }
-
-        uint8_t digest[SHA_DIGEST_LENGTH];
-        if (ParseSha1(pieces[0].c_str(), digest) != 0) {
-            printf("failed to parse sha1 \"%s\"\n", argv[i]);
-            return false;
-        }
-
-        sha1s->push_back(pieces[0]);
-        FileContents fc;
-        if (LoadFileContents(pieces[1].c_str(), &fc) != 0) {
-            return false;
-        }
-        files->push_back(std::move(fc));
-    }
-    return true;
-}
-
-static int FlashMode(const char* src_filename, const char* tgt_filename,
-                     const char* tgt_sha1, size_t tgt_size) {
-    return applypatch_flash(src_filename, tgt_filename, tgt_sha1, tgt_size);
-}
-
-static int PatchMode(int argc, const char** argv) {
-    FileContents bonusFc;
-    Value bonus(VAL_INVALID, "");
-
-    if (argc >= 3 && strcmp(argv[1], "-b") == 0) {
-        if (LoadFileContents(argv[2], &bonusFc) != 0) {
-            printf("failed to load bonus file %s\n", argv[2]);
-            return 1;
-        }
-        bonus.type = VAL_BLOB;
-        bonus.data = std::string(bonusFc.data.cbegin(), bonusFc.data.cend());
-        argc -= 2;
-        argv += 2;
-    }
-
-    if (argc < 4) {
-        return 2;
-    }
-
-    size_t target_size;
-    if (!android::base::ParseUint(argv[4], &target_size) || target_size == 0) {
-        printf("can't parse \"%s\" as byte count\n\n", argv[4]);
-        return 1;
-    }
-
-    // If no <src-sha1>:<patch> is provided, it is in flash mode.
-    if (argc == 5) {
-        if (bonus.type != VAL_INVALID) {
-            printf("bonus file not supported in flash mode\n");
-            return 1;
-        }
-        return FlashMode(argv[1], argv[2], argv[3], target_size);
-    }
-
-    std::vector<std::string> sha1s;
-    std::vector<FileContents> files;
-    if (!ParsePatchArgs(argc-5, argv+5, &sha1s, &files)) {
-        printf("failed to parse patch args\n");
-        return 1;
-    }
-
-    std::vector<std::unique_ptr<Value>> patches;
-    for (size_t i = 0; i < files.size(); ++i) {
-        patches.push_back(std::make_unique<Value>(
-                VAL_BLOB, std::string(files[i].data.cbegin(), files[i].data.cend())));
-    }
-    return applypatch(argv[1], argv[2], argv[3], target_size, sha1s, patches, &bonus);
-}
-
-// This program (applypatch) applies binary patches to files in a way that
-// is safe (the original file is not touched until we have the desired
-// replacement for it) and idempotent (it's okay to run this program
-// multiple times).
-//
-// - if the sha1 hash of <tgt-file> is <tgt-sha1>, does nothing and exits
-//   successfully.
-//
-// - otherwise, if no <src-sha1>:<patch> is provided, flashes <tgt-file> with
-//   <src-file>. <tgt-file> must be a partition name, while <src-file> must
-//   be a regular image file. <src-file> will not be deleted on success.
-//
-// - otherwise, if the sha1 hash of <src-file> is <src-sha1>, applies the
-//   bsdiff <patch> to <src-file> to produce a new file (the type of patch
-//   is automatically detected from the file header).  If that new
-//   file has sha1 hash <tgt-sha1>, moves it to replace <tgt-file>, and
-//   exits successfully.  Note that if <src-file> and <tgt-file> are
-//   not the same, <src-file> is NOT deleted on success.  <tgt-file>
-//   may be the string "-" to mean "the same as src-file".
-//
-// - otherwise, or if any error is encountered, exits with non-zero
-//   status.
-//
-// <src-file> (or <file> in check mode) may refer to an EMMC partition
-// to read the source data.  See the comments for the
-// LoadPartitionContents() function for the format of such a filename.
-
-int applypatch_modes(int argc, const char** argv) {
-    if (argc < 2) {
-      usage:
-        printf(
-            "usage: %s [-b <bonus-file>] <src-file> <tgt-file> <tgt-sha1> <tgt-size> "
-            "[<src-sha1>:<patch> ...]\n"
-            "   or  %s -c <file> [<sha1> ...]\n"
-            "   or  %s -l\n"
-            "\n"
-            "Filenames may be of the form\n"
-            "  EMMC:<partition>:<len_1>:<sha1_1>:<len_2>:<sha1_2>:...\n"
-            "to specify reading from or writing to an EMMC partition.\n\n",
-            argv[0], argv[0], argv[0]);
-        return 2;
-    }
-
-    int result;
-
-    if (strncmp(argv[1], "-l", 3) == 0) {
-        result = ShowLicenses();
-    } else if (strncmp(argv[1], "-c", 3) == 0) {
-        result = CheckMode(argc, argv);
-    } else {
-        result = PatchMode(argc, argv);
-    }
-
-    if (result == 2) {
-        goto usage;
-    }
-    return result;
+  Usage();
+  return 2;
 }
diff --git a/applypatch/applypatch_modes.h b/applypatch/applypatch_modes.h
index 3d9d08d..aa60a43 100644
--- a/applypatch/applypatch_modes.h
+++ b/applypatch/applypatch_modes.h
@@ -17,6 +17,6 @@
 #ifndef _APPLYPATCH_MODES_H
 #define _APPLYPATCH_MODES_H
 
-int applypatch_modes(int argc, const char** argv);
+int applypatch_modes(int argc, char* argv[]);
 
 #endif // _APPLYPATCH_MODES_H
diff --git a/applypatch/bspatch.cpp b/applypatch/bspatch.cpp
index 912dbbd..ba33c3a 100644
--- a/applypatch/bspatch.cpp
+++ b/applypatch/bspatch.cpp
@@ -66,18 +66,12 @@
 }
 
 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);
-    if (ctx) SHA1_Update(ctx, data, len);
-    return len;
-  };
-
+                     size_t patch_offset, SinkFn sink) {
   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);
+                               patch.data.size() - patch_offset, sink);
   if (result != 0) {
     LOG(ERROR) << "bspatch failed, result: " << result;
     // print SHA1 of the patch in the case of a data error.
diff --git a/applypatch/freecache.cpp b/applypatch/freecache.cpp
index ea364d8..e487865 100644
--- a/applypatch/freecache.cpp
+++ b/applypatch/freecache.cpp
@@ -14,31 +14,35 @@
  * limitations under the License.
  */
 
+#include <dirent.h>
 #include <errno.h>
-#include <libgen.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 #include <sys/stat.h>
 #include <sys/statfs.h>
 #include <unistd.h>
-#include <dirent.h>
-#include <ctype.h>
 
+#include <algorithm>
+#include <limits>
 #include <memory>
 #include <set>
 #include <string>
 
+#include <android-base/file.h>
+#include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/stringprintf.h>
+#include <android-base/strings.h>
 
 #include "applypatch/applypatch.h"
-#include "otautil/cache_location.h"
+#include "otautil/paths.h"
 
-static int EliminateOpenFiles(std::set<std::string>* files) {
+static int EliminateOpenFiles(const std::string& dirname, std::set<std::string>* files) {
   std::unique_ptr<DIR, decltype(&closedir)> d(opendir("/proc"), closedir);
   if (!d) {
-    printf("error opening /proc: %s\n", strerror(errno));
+    PLOG(ERROR) << "Failed to open /proc";
     return -1;
   }
   struct dirent* de;
@@ -52,7 +56,7 @@
     struct dirent* fdde;
     std::unique_ptr<DIR, decltype(&closedir)> fdd(opendir(path.c_str()), closedir);
     if (!fdd) {
-      printf("error opening %s: %s\n", path.c_str(), strerror(errno));
+      PLOG(ERROR) << "Failed to open " << path;
       continue;
     }
     while ((fdde = readdir(fdd.get())) != 0) {
@@ -62,9 +66,9 @@
       int count = readlink(fd_path.c_str(), link, sizeof(link)-1);
       if (count >= 0) {
         link[count] = '\0';
-        if (strncmp(link, "/cache/", 7) == 0) {
+        if (android::base::StartsWith(link, dirname)) {
           if (files->erase(link) > 0) {
-            printf("%s is open by %s\n", link, de->d_name);
+            LOG(INFO) << link << " is open by " << de->d_name;
           }
         }
       }
@@ -73,77 +77,163 @@
   return 0;
 }
 
-static std::set<std::string> FindExpendableFiles() {
-  std::set<std::string> files;
-  // We're allowed to delete unopened regular files in any of these
-  // directories.
-  const char* dirs[2] = {"/cache", "/cache/recovery/otatest"};
+static std::vector<std::string> FindExpendableFiles(
+    const std::string& dirname, const std::function<bool(const std::string&)>& name_filter) {
+  std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir);
+  if (!d) {
+    PLOG(ERROR) << "Failed to open " << dirname;
+    return {};
+  }
 
-  for (size_t i = 0; i < sizeof(dirs)/sizeof(dirs[0]); ++i) {
-    std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirs[i]), closedir);
-    if (!d) {
-      printf("error opening %s: %s\n", dirs[i], strerror(errno));
+  // Look for regular files in the directory (not in any subdirectories).
+  std::set<std::string> files;
+  struct dirent* de;
+  while ((de = readdir(d.get())) != 0) {
+    std::string path = dirname + "/" + 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 == Paths::Get().cache_temp_source()) {
       continue;
     }
 
-    // Look for regular files in the directory (not in any subdirectories).
-    struct dirent* de;
-    while ((de = readdir(d.get())) != 0) {
-      std::string path = std::string(dirs[i]) + "/" + de->d_name;
+    // Do not delete the file if it doesn't have the expected format.
+    if (name_filter != nullptr && !name_filter(de->d_name)) {
+      continue;
+    }
 
-      // 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;
-      }
-
-      struct stat st;
-      if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
-        files.insert(path);
-      }
+    struct stat st;
+    if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+      files.insert(path);
     }
   }
 
-  printf("%zu regular files in deletable directories\n", files.size());
-  if (EliminateOpenFiles(&files) < 0) {
-    return std::set<std::string>();
+  LOG(INFO) << files.size() << " regular files in deletable directory";
+  if (EliminateOpenFiles(dirname, &files) < 0) {
+    return {};
   }
-  return files;
+
+  return std::vector<std::string>(files.begin(), files.end());
 }
 
-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);
-
-  if (free_now >= bytes_needed) {
+// Parses the index of given log file, e.g. 3 for last_log.3; returns max number if the log name
+// doesn't have the expected format so that we'll delete these ones first.
+static unsigned int GetLogIndex(const std::string& log_name) {
+  if (log_name == "last_log" || log_name == "last_kmsg") {
     return 0;
   }
-  std::set<std::string> files = FindExpendableFiles();
-  if (files.empty()) {
-    // nothing we can delete to free up space!
-    printf("no files can be deleted to free space on /cache\n");
+
+  unsigned int index;
+  if (sscanf(log_name.c_str(), "last_log.%u", &index) == 1 ||
+      sscanf(log_name.c_str(), "last_kmsg.%u", &index) == 1) {
+    return index;
+  }
+
+  return std::numeric_limits<unsigned int>::max();
+}
+
+// Returns the amount of free space (in bytes) on the filesystem containing filename, or -1 on
+// error.
+static int64_t FreeSpaceForFile(const std::string& filename) {
+  struct statfs sf;
+  if (statfs(filename.c_str(), &sf) == -1) {
+    PLOG(ERROR) << "Failed to statfs " << filename;
     return -1;
   }
 
-  // We could try to be smarter about which files to delete:  the
-  // biggest ones?  the smallest ones that will free up enough space?
-  // the oldest?  the newest?
-  //
-  // Instead, we'll be dumb.
+  int64_t free_space = static_cast<int64_t>(sf.f_bsize) * sf.f_bavail;
+  if (sf.f_bsize == 0 || free_space / sf.f_bsize != sf.f_bavail) {
+    LOG(ERROR) << "Invalid block size or overflow (sf.f_bsize " << sf.f_bsize << ", sf.f_bavail "
+               << sf.f_bavail << ")";
+    return -1;
+  }
+  return free_space;
+}
 
-  for (const auto& file : files) {
-    unlink(file.c_str());
-    free_now = FreeSpaceForFile("/cache");
-    printf("deleted %s; now %zu bytes free\n", file.c_str(), free_now);
-    if (free_now < bytes_needed) {
-        break;
+bool CheckAndFreeSpaceOnCache(size_t bytes) {
+#ifndef __ANDROID__
+  // TODO(xunchang): Implement a heuristic cache size check during host simulation.
+  LOG(WARNING) << "Skipped making (" << bytes
+               << ") bytes free space on /cache; program is running on host";
+  return true;
+#endif
+
+  std::vector<std::string> dirs{ "/cache", Paths::Get().cache_log_directory() };
+  for (const auto& dirname : dirs) {
+    if (RemoveFilesInDirectory(bytes, dirname, FreeSpaceForFile)) {
+      return true;
     }
   }
-  return (free_now >= bytes_needed) ? 0 : -1;
+
+  return false;
+}
+
+bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname,
+                            const std::function<int64_t(const std::string&)>& space_checker) {
+  struct stat st;
+  if (stat(dirname.c_str(), &st) == -1) {
+    PLOG(ERROR) << "Failed to stat " << dirname;
+    return false;
+  }
+  if (!S_ISDIR(st.st_mode)) {
+    LOG(ERROR) << dirname << " is not a directory";
+    return false;
+  }
+
+  int64_t free_now = space_checker(dirname);
+  if (free_now == -1) {
+    LOG(ERROR) << "Failed to check free space for " << dirname;
+    return false;
+  }
+  LOG(INFO) << free_now << " bytes free on " << dirname << " (" << bytes_needed << " needed)";
+
+  if (free_now >= bytes_needed) {
+    return true;
+  }
+
+  std::vector<std::string> files;
+  if (dirname == Paths::Get().cache_log_directory()) {
+    // Deletes the log files only.
+    auto log_filter = [](const std::string& file_name) {
+      return android::base::StartsWith(file_name, "last_log") ||
+             android::base::StartsWith(file_name, "last_kmsg");
+    };
+
+    files = FindExpendableFiles(dirname, log_filter);
+
+    // Older logs will come to the top of the queue.
+    auto comparator = [](const std::string& name1, const std::string& name2) -> bool {
+      unsigned int index1 = GetLogIndex(android::base::Basename(name1));
+      unsigned int index2 = GetLogIndex(android::base::Basename(name2));
+      if (index1 == index2) {
+        return name1 < name2;
+      }
+
+      return index1 > index2;
+    };
+
+    std::sort(files.begin(), files.end(), comparator);
+  } else {
+    // We're allowed to delete unopened regular files in the directory.
+    files = FindExpendableFiles(dirname, nullptr);
+  }
+
+  for (const auto& file : files) {
+    if (unlink(file.c_str()) == -1) {
+      PLOG(ERROR) << "Failed to delete " << file;
+      continue;
+    }
+
+    free_now = space_checker(dirname);
+    if (free_now == -1) {
+      LOG(ERROR) << "Failed to check free space for " << dirname;
+      return false;
+    }
+    LOG(INFO) << "Deleted " << file << "; now " << free_now << " bytes free";
+    if (free_now >= bytes_needed) {
+      return true;
+    }
+  }
+
+  return false;
 }
diff --git a/applypatch/imgdiff.cpp b/applypatch/imgdiff.cpp
index 674cc2b..415d95f 100644
--- a/applypatch/imgdiff.cpp
+++ b/applypatch/imgdiff.cpp
@@ -462,12 +462,12 @@
       target_len_(tgt.GetRawDataLength()),
       target_uncompressed_len_(tgt.DataLengthForPatch()),
       target_compress_level_(tgt.GetCompressLevel()),
-      data_(tgt.DataForPatch(), tgt.DataForPatch() + tgt.DataLengthForPatch()) {}
+      data_(tgt.GetRawData(), tgt.GetRawData() + tgt.GetRawDataLength()) {}
 
 // 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));
+  return target_len < patch_size || (tgt.GetType() == CHUNK_NORMAL && target_len <= 160);
 }
 
 void PatchChunk::UpdateSourceOffset(const SortedRangeSet& src_range) {
diff --git a/applypatch/imgpatch.cpp b/applypatch/imgpatch.cpp
index 3682d61..e6be39a 100644
--- a/applypatch/imgpatch.cpp
+++ b/applypatch/imgpatch.cpp
@@ -38,6 +38,7 @@
 #include <zlib.h>
 
 #include "edify/expr.h"
+#include "otautil/print_sha1.h"
 
 static inline int64_t Read8(const void *address) {
   return android::base::get_unaligned<int64_t>(address);
@@ -51,8 +52,9 @@
 // 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 char* deflate_header, SinkFn sink, SHA_CTX* ctx) {
+                                            const char* deflate_header, SinkFn sink) {
   size_t expected_target_length = static_cast<size_t>(Read8(deflate_header + 32));
+  CHECK_GT(expected_target_length, 0);
   int level = Read4(deflate_header + 40);
   int method = Read4(deflate_header + 44);
   int window_bits = Read4(deflate_header + 48);
@@ -77,7 +79,7 @@
   size_t total_written = 0;
   static constexpr size_t buffer_size = 32768;
   auto compression_sink = [&strm, &actual_target_length, &expected_target_length, &total_written,
-                           &ret, &ctx, &sink](const uint8_t* data, size_t len) -> size_t {
+                           &ret, &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;
@@ -102,15 +104,13 @@
         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);
 
     actual_target_length += len;
     return len;
   };
 
-  int bspatch_result =
-      ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink, nullptr);
+  int bspatch_result = ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink);
   deflateEnd(&strm);
 
   if (bspatch_result != 0) {
@@ -127,19 +127,20 @@
                << actual_target_length;
     return false;
   }
-  LOG(DEBUG) << "bspatch writes " << total_written << " bytes in total to streaming output.";
+  LOG(DEBUG) << "bspatch wrote " << total_written << " bytes in total to streaming output.";
 
   return true;
 }
 
 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);
+  Value patch(Value::Type::BLOB,
+              std::string(reinterpret_cast<const char*>(patch_data), patch_size));
+  return ApplyImagePatch(old_data, old_size, patch, sink, nullptr);
 }
 
 int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink,
-                    SHA_CTX* ctx, const Value* bonus_data) {
+                    const Value* bonus_data) {
   if (patch.data.size() < 12) {
     printf("patch too short to contain header\n");
     return -1;
@@ -180,10 +181,12 @@
         printf("source data too short\n");
         return -1;
       }
-      if (ApplyBSDiffPatch(old_data + src_start, src_len, patch, patch_offset, sink, ctx) != 0) {
+      if (ApplyBSDiffPatch(old_data + src_start, src_len, patch, patch_offset, sink) != 0) {
         printf("Failed to apply bsdiff patch.\n");
         return -1;
       }
+
+      LOG(DEBUG) << "Processed chunk type normal";
     } else if (type == CHUNK_RAW) {
       const char* raw_header = patch_header + pos;
       pos += 4;
@@ -198,14 +201,13 @@
         printf("failed to read chunk %d raw data\n", i);
         return -1;
       }
-      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;
+
+      LOG(DEBUG) << "Processed chunk type raw";
     } else if (type == CHUNK_DEFLATE) {
       // deflate chunks have an additional 60 bytes in their chunk header.
       const char* deflate_header = patch_header + pos;
@@ -228,11 +230,10 @@
       // Decompress the source data; the chunk header tells us exactly
       // how big we expect it to be when decompressed.
 
-      // Note: expanded_len will include the bonus data size if
-      // the patch was constructed with bonus data.  The
-      // deflation will come up 'bonus_size' bytes short; these
-      // must be appended from the bonus_data value.
-      size_t bonus_size = (i == 1 && bonus_data != NULL) ? bonus_data->data.size() : 0;
+      // Note: expanded_len will include the bonus data size if the patch was constructed with
+      // bonus data. The deflation will come up 'bonus_size' bytes short; these must be appended
+      // from the bonus_data value.
+      size_t bonus_size = (i == 1 && bonus_data != nullptr) ? bonus_data->data.size() : 0;
 
       std::vector<unsigned char> expanded_source(expanded_len);
 
@@ -270,17 +271,18 @@
         inflateEnd(&strm);
 
         if (bonus_size) {
-          memcpy(expanded_source.data() + (expanded_len - bonus_size), &bonus_data->data[0],
+          memcpy(expanded_source.data() + (expanded_len - bonus_size), bonus_data->data.data(),
                  bonus_size);
         }
       }
 
       if (!ApplyBSDiffPatchAndStreamOutput(expanded_source.data(), expanded_len, patch,
-                                           patch_offset, deflate_header, sink, ctx)) {
+                                           patch_offset, deflate_header, sink)) {
         LOG(ERROR) << "Fail to apply streaming bspatch.";
         return -1;
       }
 
+      LOG(DEBUG) << "Processed chunk type deflate";
     } else {
       printf("patch chunk %d is unknown type %d\n", i, type);
       return -1;
diff --git a/applypatch/include/applypatch/applypatch.h b/applypatch/include/applypatch/applypatch.h
index 912ead1..6fc6f0f 100644
--- a/applypatch/include/applypatch/applypatch.h
+++ b/applypatch/include/applypatch/applypatch.h
@@ -21,6 +21,7 @@
 
 #include <functional>
 #include <memory>
+#include <ostream>
 #include <string>
 #include <vector>
 
@@ -39,45 +40,91 @@
 // applypatch.cpp
 
 int ShowLicenses();
-size_t FreeSpaceForFile(const char* filename);
-int CacheSizeCheck(size_t bytes);
-int ParseSha1(const char* str, uint8_t* digest);
 
-int applypatch(const char* source_filename,
-               const char* target_filename,
-               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);
-int applypatch_check(const char* filename,
-                     const std::vector<std::string>& patch_sha1_str);
-int applypatch_flash(const char* source_filename, const char* target_filename,
-                     const char* target_sha1_str, size_t target_size);
+// Parses a given string of 40 hex digits into 20-byte array 'digest'. 'str' may contain only the
+// digest or be of the form "<digest>:<anything>". Returns 0 on success, or -1 on any error.
+int ParseSha1(const std::string& str, uint8_t* digest);
 
-int LoadFileContents(const char* filename, FileContents* file);
-int SaveFileContents(const char* filename, const FileContents* file);
+struct Partition {
+  Partition() = default;
+
+  Partition(const std::string& name, size_t size, const std::string& hash)
+      : name(name), size(size), hash(hash) {}
+
+  // Parses and returns the given string into a Partition object. The input string is of the form
+  // "EMMC:<device>:<size>:<hash>". Returns the parsed Partition, or an empty object on error.
+  static Partition Parse(const std::string& partition, std::string* err);
+
+  std::string ToString() const;
+
+  // Returns whether the current Partition object is valid.
+  explicit operator bool() const {
+    return !name.empty();
+  }
+
+  std::string name;
+  size_t size;
+  std::string hash;
+};
+
+std::ostream& operator<<(std::ostream& os, const Partition& partition);
+
+// Applies the given 'patch' to the 'source' Partition, verifies then writes the patching result to
+// the 'target' Partition. While patching, it will backup the data on the source partition to
+// /cache, so that the patching could be resumed on interruption even if both of the source and
+// target partitions refer to the same device. The function is idempotent if called multiple times.
+// An optional arg 'bonus' can be provided, if the patch was generated with a bonus output.
+// Returns the patching result.
+bool PatchPartition(const Partition& target, const Partition& source, const Value& patch,
+                    const Value* bonus);
+
+// Returns whether the contents of the eMMC target or the cached file match the embedded hash.
+// It will look for the backup on /cache if the given partition doesn't match the checksum.
+bool PatchPartitionCheck(const Partition& target, const Partition& source);
+
+// Checks whether the contents of the given partition has the desired hash. It will NOT look for
+// the backup on /cache if the given partition doesn't have the expected checksum.
+bool CheckPartition(const Partition& target);
+
+// Flashes a given image in 'source_filename' to the eMMC target partition. It verifies the target
+// checksum first, and will return if target already has the desired hash. Otherwise it checks the
+// checksum of the given source image, flashes, and verifies the target partition afterwards. The
+// function is idempotent. Returns the flashing result.
+bool FlashPartition(const Partition& target, const std::string& source_filename);
+
+// Reads a file into memory; stores the file contents and associated metadata in *file.
+bool LoadFileContents(const std::string& filename, FileContents* file);
+
+// Saves the given FileContents object to the given filename.
+bool SaveFileContents(const std::string& filename, const FileContents* file);
 
 // bspatch.cpp
 
 void ShowBSDiffLicense();
 
 // 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.
+// data given by (old_data, old_size). Writes the patched output through the given 'sink'. 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);
+                     size_t patch_offset, SinkFn sink);
 
 // imgpatch.cpp
 
 // 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.
+// the optional bonus data. Writes the patched output through the given 'sink'. 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);
+                    const Value* bonus_data);
 
 // freecache.cpp
 
-int MakeFreeSpaceOnCache(size_t bytes_needed);
+// Checks whether /cache partition has at least 'bytes'-byte free space. Returns true immediately
+// if so. Otherwise, it will try to free some space by removing older logs, checks again and
+// returns the checking result.
+bool CheckAndFreeSpaceOnCache(size_t bytes);
 
+// Removes the files in |dirname| until we have at least |bytes_needed| bytes of free space on the
+// partition. |space_checker| should return the size of the free space, or -1 on error.
+bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname,
+                            const std::function<int64_t(const std::string&)>& space_checker);
 #endif
diff --git a/applypatch/include/applypatch/imgdiff_image.h b/applypatch/include/applypatch/imgdiff_image.h
index 0848072..6716051 100644
--- a/applypatch/include/applypatch/imgdiff_image.h
+++ b/applypatch/include/applypatch/imgdiff_image.h
@@ -44,6 +44,8 @@
   int GetType() const {
     return type_;
   }
+
+  const uint8_t* GetRawData() const;
   size_t GetRawDataLength() const {
     return raw_data_len_;
   }
@@ -99,7 +101,6 @@
                         bsdiff::SuffixArrayIndexInterface** bsdiff_cache);
 
  private:
-  const uint8_t* GetRawData() const;
   bool TryReconstruction(int level);
 
   int type_;                                    // CHUNK_NORMAL, CHUNK_DEFLATE, CHUNK_RAW
diff --git a/boot_control/Android.bp b/boot_control/Android.bp
new file mode 100644
index 0000000..7720ead
--- /dev/null
+++ b/boot_control/Android.bp
@@ -0,0 +1,37 @@
+//
+// 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.
+//
+
+cc_library_shared {
+    name: "bootctrl.default",
+    recovery_available: true,
+    relative_install_path: "hw",
+
+    srcs: ["boot_control.cpp"],
+
+    cflags: [
+        "-D_FILE_OFFSET_BITS=64",
+        "-Werror",
+        "-Wall",
+        "-Wextra",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libbootloader_message",
+        "libfs_mgr",
+        "liblog",
+    ],
+}
diff --git a/boot_control/Android.mk b/boot_control/Android.mk
deleted file mode 100644
index 9814d71..0000000
--- a/boot_control/Android.mk
+++ /dev/null
@@ -1,33 +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.
-#
-
-LOCAL_PATH := $(my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := bootctrl.bcb
-LOCAL_MODULE_RELATIVE_PATH := hw
-LOCAL_SRC_FILES := boot_control.cpp
-LOCAL_CFLAGS := \
-  -D_FILE_OFFSET_BITS=64 \
-  -Werror \
-  -Wall \
-  -Wextra
-LOCAL_SHARED_LIBRARIES := liblog
-LOCAL_STATIC_LIBRARIES := libbootloader_message libfs_mgr libbase
-LOCAL_POST_INSTALL_CMD := \
-  $(hide) mkdir -p $(TARGET_OUT_SHARED_LIBRARIES)/hw && \
-  ln -sf bootctrl.bcb.so $(TARGET_OUT_SHARED_LIBRARIES)/hw/bootctrl.default.so
-include $(BUILD_SHARED_LIBRARY)
diff --git a/bootloader_message/Android.bp b/bootloader_message/Android.bp
index c81c67b..5cd2132 100644
--- a/bootloader_message/Android.bp
+++ b/bootloader_message/Android.bp
@@ -14,14 +14,15 @@
 // limitations under the License.
 //
 
-cc_library_static {
+cc_library {
     name: "libbootloader_message",
+    recovery_available: true,
     srcs: ["bootloader_message.cpp"],
     cflags: [
         "-Wall",
         "-Werror",
     ],
-    static_libs: [
+    shared_libs: [
         "libbase",
         "libfs_mgr",
     ],
diff --git a/common.h b/common.h
index 8b336f8..38bdd52 100644
--- a/common.h
+++ b/common.h
@@ -17,8 +17,8 @@
 #ifndef RECOVERY_COMMON_H
 #define RECOVERY_COMMON_H
 
-#include <stdio.h>
 #include <stdarg.h>
+#include <stdio.h>
 
 #include <string>
 
@@ -27,9 +27,12 @@
 static constexpr int kRecoveryApiVersion = 3;
 
 class RecoveryUI;
+struct selabel_handle;
 
+extern struct selabel_handle* sehandle;
 extern RecoveryUI* ui;
 extern bool modified_flash;
+extern bool has_cache;
 
 // The current stage, e.g. "1/2".
 extern std::string stage;
@@ -37,13 +40,9 @@
 // The reason argument provided in "--reason=".
 extern const char* reason;
 
-// fopen a file, mounting volumes and making parent dirs as necessary.
-FILE* fopen_path(const char *path, const char *mode);
-
-void ui_print(const char* format, ...);
+void ui_print(const char* format, ...) __printflike(1, 2);
 
 bool is_ro_debuggable();
 
-bool reboot(const std::string& command);
-
+bool SetUsbConfig(const std::string& state);
 #endif  // RECOVERY_COMMON_H
diff --git a/device.cpp b/device.cpp
index f881daf..eec1932 100644
--- a/device.cpp
+++ b/device.cpp
@@ -16,50 +16,58 @@
 
 #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",
-#ifndef AB_OTA_UPDATER
-  "Wipe cache partition",
-#endif  // !AB_OTA_UPDATER
-  "Mount /system",
-  "View recovery logs",
-  "Run graphics test",
-  "Run locale test",
-  "Power off",
-  nullptr,
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <android-base/logging.h>
+
+#include "ui.h"
+
+static std::vector<std::pair<std::string, Device::BuiltinAction>> g_menu_actions{
+  { "Reboot system now", Device::REBOOT },
+  { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
+  { "Enter fastboot", Device::ENTER_FASTBOOT },
+  { "Apply update from ADB", Device::APPLY_ADB_SIDELOAD },
+  { "Apply update from SD card", Device::APPLY_SDCARD },
+  { "Wipe data/factory reset", Device::WIPE_DATA },
+  { "Wipe cache partition", Device::WIPE_CACHE },
+  { "Mount /system", Device::MOUNT_SYSTEM },
+  { "View recovery logs", Device::VIEW_RECOVERY_LOGS },
+  { "Run graphics test", Device::RUN_GRAPHICS_TEST },
+  { "Run locale test", Device::RUN_LOCALE_TEST },
+  { "Power off", Device::SHUTDOWN },
 };
 
-static const Device::BuiltinAction MENU_ACTIONS[] = {
-  Device::REBOOT,
-  Device::REBOOT_BOOTLOADER,
-  Device::APPLY_ADB_SIDELOAD,
-  Device::APPLY_SDCARD,
-  Device::WIPE_DATA,
-#ifndef AB_OTA_UPDATER
-  Device::WIPE_CACHE,
-#endif  // !AB_OTA_UPDATER
-  Device::MOUNT_SYSTEM,
-  Device::VIEW_RECOVERY_LOGS,
-  Device::RUN_GRAPHICS_TEST,
-  Device::RUN_LOCALE_TEST,
-  Device::SHUTDOWN,
-};
+static std::vector<std::string> g_menu_items;
 
-static_assert(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]) ==
-              sizeof(MENU_ACTIONS) / sizeof(MENU_ACTIONS[0]) + 1,
-              "MENU_ITEMS and MENU_ACTIONS should have the same length, "
-              "except for the extra NULL entry in MENU_ITEMS.");
-
-const char* const* Device::GetMenuItems() {
-  return MENU_ITEMS;
+static void PopulateMenuItems() {
+  g_menu_items.clear();
+  std::transform(g_menu_actions.cbegin(), g_menu_actions.cend(), std::back_inserter(g_menu_items),
+                 [](const auto& entry) { return entry.first; });
 }
 
-Device::BuiltinAction Device::InvokeMenuItem(int menu_position) {
-  return menu_position < 0 ? NO_ACTION : MENU_ACTIONS[menu_position];
+Device::Device(RecoveryUI* ui) : ui_(ui) {
+  PopulateMenuItems();
+}
+
+void Device::RemoveMenuItemForAction(Device::BuiltinAction action) {
+  g_menu_actions.erase(
+      std::remove_if(g_menu_actions.begin(), g_menu_actions.end(),
+                     [action](const auto& entry) { return entry.second == action; }));
+  CHECK(!g_menu_actions.empty());
+
+  // Re-populate the menu items.
+  PopulateMenuItems();
+}
+
+const std::vector<std::string>& Device::GetMenuItems() {
+  return g_menu_items;
+}
+
+Device::BuiltinAction Device::InvokeMenuItem(size_t menu_position) {
+  return g_menu_actions[menu_position].second;
 }
 
 int Device::HandleMenuKey(int key, bool visible) {
diff --git a/device.h b/device.h
index 74745b3..6a8daf8 100644
--- a/device.h
+++ b/device.h
@@ -17,41 +17,21 @@
 #ifndef _RECOVERY_DEVICE_H
 #define _RECOVERY_DEVICE_H
 
-#include "ui.h"
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+// Forward declaration to avoid including "ui.h".
+class RecoveryUI;
 
 class Device {
  public:
-  explicit Device(RecoveryUI* ui) : ui_(ui) {}
-  virtual ~Device() {}
-
-  // Called to obtain the UI object that should be used to display the recovery user interface for
-  // this device. You should not have called Init() on the UI object already, the caller will do
-  // that after this method returns.
-  virtual RecoveryUI* GetUI() {
-    return ui_;
-  }
-
-  // Called when recovery starts up (after the UI has been obtained and initialized and after the
-  // arguments have been parsed, but before anything else).
-  virtual void StartRecovery() {};
-
-  // Called from the main thread when recovery is at the main menu and waiting for input, and a key
-  // is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
-  // recovery will be at the main menu with it invisible after an unsuccessful operation [ie OTA
-  // package failure], or if recovery is started with no command.)
-  //
-  // 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
-  // object you returned from GetUI if you want to find out if other keys are held down.)
-  //
-  // 'visible' is true if the menu is visible.
-  //
-  // Returns one of the defined constants below in order to:
-  //
-  //   - move the menu highlight (kHighlight{Up,Down})
-  //   - invoke the highlighted item (kInvokeItem)
-  //   - do nothing (kNoAction)
-  //   - invoke a specific action (a menu position: any non-negative number)
-  virtual int HandleMenuKey(int key, bool visible);
+  static constexpr const int kNoAction = -1;
+  static constexpr const int kHighlightUp = -2;
+  static constexpr const int kHighlightDown = -3;
+  static constexpr const int kInvokeItem = -4;
 
   enum BuiltinAction {
     NO_ACTION = 0,
@@ -67,24 +47,61 @@
     MOUNT_SYSTEM = 10,
     RUN_GRAPHICS_TEST = 11,
     RUN_LOCALE_TEST = 12,
+    KEY_INTERRUPTED = 13,
+    ENTER_FASTBOOT = 14,
+    ENTER_RECOVERY = 15,
   };
 
-  // Return the list of menu items (an array of strings, NULL-terminated). The menu_position passed
-  // to InvokeMenuItem will correspond to the indexes into this array.
-  virtual const char* const* GetMenuItems();
+  explicit Device(RecoveryUI* ui);
+  virtual ~Device() {}
 
-  // Perform a recovery action selected from the menu. 'menu_position' will be the item number of
-  // the selected menu item, or a non-negative number returned from HandleMenuKey(). The menu will
-  // be hidden when this is called; implementations can call ui_print() to print information to the
+  // Returns a raw pointer to the RecoveryUI object.
+  virtual RecoveryUI* GetUI() {
+    return ui_.get();
+  }
+
+  // Resets the UI object to the given UI. Used to override the default UI in case initialization
+  // failed, or we want a different UI for some reason. The device object will take the ownership.
+  virtual void ResetUI(RecoveryUI* ui) {
+    ui_.reset(ui);
+  }
+
+  // Called when recovery starts up (after the UI has been obtained and initialized and after the
+  // arguments have been parsed, but before anything else).
+  virtual void StartRecovery() {};
+
+  // Called from the main thread when recovery is at the main menu and waiting for input, and a key
+  // is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
+  // recovery will be at the main menu with it invisible after an unsuccessful operation, such as
+  // failed to install an OTA package, or if recovery is started with no command.)
+  //
+  // 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
+  // object you returned from GetUI() if you want to find out if other keys are held down.)
+  //
+  // 'visible' is true if the menu is visible.
+  //
+  // Returns one of the defined constants below in order to:
+  //   - move the menu highlight (kHighlight{Up,Down}: negative value)
+  //   - invoke the highlighted item (kInvokeItem: negative value)
+  //   - do nothing (kNoAction: negative value)
+  //   - invoke a specific action (a menu position: non-negative value)
+  virtual int HandleMenuKey(int key, bool visible);
+
+  // Returns the list of menu items (a vector of strings). The menu_position passed to
+  // InvokeMenuItem() will correspond to the indexes into this array.
+  virtual const std::vector<std::string>& GetMenuItems();
+
+  // Performs a recovery action selected from the menu. 'menu_position' will be the index of the
+  // selected menu item, or a non-negative value returned from HandleMenuKey(). The menu will be
+  // hidden when this is called; implementations can call ui_print() to print information to the
   // screen. If the menu position is one of the builtin actions, you can just return the
   // corresponding enum value. If it is an action specific to your device, you actually perform it
   // here and return NO_ACTION.
-  virtual BuiltinAction InvokeMenuItem(int menu_position);
+  virtual BuiltinAction InvokeMenuItem(size_t menu_position);
 
-  static const int kNoAction = -1;
-  static const int kHighlightUp = -2;
-  static const int kHighlightDown = -3;
-  static const int kInvokeItem = -4;
+  // Removes the menu item for the given action. This allows tailoring the menu based on the
+  // runtime info, such as the availability of /cache or /sdcard.
+  virtual void RemoveMenuItemForAction(Device::BuiltinAction action);
 
   // Called before and after we do a wipe data/factory reset operation, either via a reboot from the
   // main system with the --wipe_data flag, or when the user boots into recovery image manually and
@@ -100,11 +117,16 @@
   }
 
  private:
-  RecoveryUI* ui_;
+  // The RecoveryUI object that should be used to display the user interface for this device.
+  std::unique_ptr<RecoveryUI> ui_;
 };
 
+// Disable name mangling, as this function will be loaded via dlsym(3).
+extern "C" {
+
 // The device-specific library must define this function (or the default one will be used, if there
 // is no device-specific library). It returns the Device object that recovery should use.
 Device* make_device();
+}
 
 #endif  // _DEVICE_H
diff --git a/edify/expr.cpp b/edify/expr.cpp
index 6823b73..c090eb2 100644
--- a/edify/expr.cpp
+++ b/edify/expr.cpp
@@ -51,9 +51,9 @@
     if (!v) {
         return false;
     }
-    if (v->type != VAL_STRING) {
-        ErrorAbort(state, kArgsParsingFailure, "expecting string, got value type %d", v->type);
-        return false;
+    if (v->type != Value::Type::STRING) {
+      ErrorAbort(state, kArgsParsingFailure, "expecting string, got value type %d", v->type);
+      return false;
     }
 
     *result = v->data;
@@ -68,7 +68,7 @@
     if (str == nullptr) {
         return nullptr;
     }
-    return new Value(VAL_STRING, str);
+    return new Value(Value::Type::STRING, str);
 }
 
 Value* StringValue(const std::string& str) {
diff --git a/edify/include/edify/expr.h b/edify/include/edify/expr.h
index 770d1cf..5cbd5e1 100644
--- a/edify/include/edify/expr.h
+++ b/edify/include/edify/expr.h
@@ -53,19 +53,16 @@
   bool is_retry = false;
 };
 
-enum ValueType {
-    VAL_INVALID = -1,
-    VAL_STRING = 1,
-    VAL_BLOB = 2,
-};
-
 struct Value {
-    ValueType type;
-    std::string data;
+  enum class Type {
+    STRING = 1,
+    BLOB = 2,
+  };
 
-    Value(ValueType type, const std::string& str) :
-        type(type),
-        data(str) {}
+  Value(Type type, const std::string& str) : type(type), data(str) {}
+
+  Type type;
+  std::string data;
 };
 
 struct Expr;
@@ -156,6 +153,6 @@
 
 Value* StringValue(const std::string& str);
 
-int parse_string(const char* str, std::unique_ptr<Expr>* root, int* error_count);
+int ParseString(const std::string& str, std::unique_ptr<Expr>* root, int* error_count);
 
 #endif  // _EXPRESSION_H
diff --git a/edify/parser.yy b/edify/parser.yy
index bd2e010..3a63c37 100644
--- a/edify/parser.yy
+++ b/edify/parser.yy
@@ -138,7 +138,7 @@
   ++*error_count;
 }
 
-int parse_string(const char* str, std::unique_ptr<Expr>* root, int* error_count) {
-    yy_switch_to_buffer(yy_scan_string(str));
-    return yyparse(root, error_count);
+int ParseString(const std::string& str, std::unique_ptr<Expr>* root, int* error_count) {
+  yy_switch_to_buffer(yy_scan_string(str.c_str()));
+  return yyparse(root, error_count);
 }
diff --git a/etc/init.rc b/etc/init.rc
index 0fc6c4c..fa8fe26 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -6,11 +6,17 @@
 
     start ueventd
 
+    setprop sys.usb.configfs 0
+
 on init
     export ANDROID_ROOT /system
     export ANDROID_DATA /data
     export EXTERNAL_STORAGE /sdcard
 
+    symlink /proc/self/fd/0 /dev/stdin
+    symlink /proc/self/fd/1 /dev/stdout
+    symlink /proc/self/fd/2 /dev/stderr
+
     symlink /system/bin /bin
     symlink /system/etc /etc
 
@@ -22,6 +28,7 @@
     mkdir /data
     mkdir /cache
     mkdir /sideload
+    mkdir /mnt/system
     mount tmpfs tmpfs /tmp
 
     chown root shell /tmp
@@ -30,20 +37,6 @@
     write /proc/sys/kernel/panic_on_oops 1
     write /proc/sys/vm/max_map_count 1000000
 
-on fs
-    write /sys/class/android_usb/android0/f_ffs/aliases adb
-    mkdir /dev/usb-ffs 0770 shell shell
-    mkdir /dev/usb-ffs/adb 0770 shell shell
-    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
-
-    write /sys/class/android_usb/android0/enable 0
-    write /sys/class/android_usb/android0/idVendor 18D1
-    write /sys/class/android_usb/android0/idProduct D001
-    write /sys/class/android_usb/android0/functions adb
-    write /sys/class/android_usb/android0/iManufacturer ${ro.product.manufacturer}
-    write /sys/class/android_usb/android0/iProduct ${ro.product.model}
-    write /sys/class/android_usb/android0/iSerial ${ro.serialno}
-
 on boot
     ifup lo
     hostname localhost
@@ -76,7 +69,7 @@
     trigger early-boot
     trigger boot
 
-service ueventd /sbin/ueventd
+service ueventd /system/bin/ueventd
     critical
     seclabel u:r:ueventd:s0
 
@@ -84,21 +77,111 @@
     critical
     seclabel u:r:charger:s0
 
-service recovery /sbin/recovery
+service recovery /system/bin/recovery
+    socket recovery stream 422 system system
     seclabel u:r:recovery:s0
 
-service adbd /sbin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery
+service adbd /system/bin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery
     disabled
     socket adbd stream 660 system system
     seclabel u:r:adbd:s0
 
-# Always start adbd on userdebug and eng builds
-on property:ro.debuggable=1
-    write /sys/class/android_usb/android0/enable 1
-    start adbd
+service fastbootd /system/bin/fastbootd
+    disabled
+    group system
+    seclabel u:r:fastbootd:s0
 
 # Restart adbd so it can run as root
 on property:service.adb.root=1
-    write /sys/class/android_usb/android0/enable 0
     restart adbd
+
+# Always start adbd on userdebug and eng builds
+on fs && property:ro.debuggable=1
+    setprop sys.usb.config adb
+
+on fs && property:sys.usb.configfs=1
+    mount configfs none /config
+    mkdir /config/usb_gadget/g1 0770 shell shell
+    write /config/usb_gadget/g1/idVendor 0x18D1
+    mkdir /config/usb_gadget/g1/strings/0x409 0770
+    write /config/usb_gadget/g1/strings/0x409/serialnumber ${ro.serialno}
+    write /config/usb_gadget/g1/strings/0x409/manufacturer ${ro.product.manufacturer}
+    write /config/usb_gadget/g1/strings/0x409/product ${ro.product.model}
+    mkdir /config/usb_gadget/g1/functions/ffs.adb
+    mkdir /config/usb_gadget/g1/functions/ffs.fastboot
+    mkdir /config/usb_gadget/g1/configs/b.1 0777 shell shell
+    mkdir /config/usb_gadget/g1/configs/b.1/strings/0x409 0770 shell shell
+
+on fs && property:sys.usb.configfs=0
+    write /sys/class/android_usb/android0/f_ffs/aliases adb,fastboot
+    write /sys/class/android_usb/android0/idVendor 18D1
+    write /sys/class/android_usb/android0/iManufacturer ${ro.product.manufacturer}
+    write /sys/class/android_usb/android0/iProduct ${ro.product.model}
+    write /sys/class/android_usb/android0/iSerial ${ro.serialno}
+
+on fs
+    mkdir /dev/usb-ffs 0775 shell shell
+    mkdir /dev/usb-ffs/adb 0770 shell shell
+    mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
+    mkdir /dev/usb-ffs/fastboot 0770 system system
+    mount functionfs fastboot /dev/usb-ffs/fastboot rmode=0770,fmode=0660,uid=1000,gid=1000
+
+on property:sys.usb.config=adb
+    start adbd
+
+on property:sys.usb.config=fastboot
+    start fastbootd
+
+on property:sys.usb.config=none && property:sys.usb.configfs=0
+    stop adbd
+    stop fastboot
+    write /sys/class/android_usb/android0/enable 0
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=adb && property:sys.usb.configfs=0
+    write /sys/class/android_usb/android0/idProduct D001
+    write /sys/class/android_usb/android0/functions adb
     write /sys/class/android_usb/android0/enable 1
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=sideload && property:sys.usb.configfs=0
+    write /sys/class/android_usb/android0/idProduct D001
+    write /sys/class/android_usb/android0/functions adb
+    write /sys/class/android_usb/android0/enable 1
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=fastboot && property:sys.usb.configfs=0
+    write /sys/class/android_usb/android0/idProduct 4EE0
+    write /sys/class/android_usb/android0/functions fastboot
+    write /sys/class/android_usb/android0/enable 1
+    setprop sys.usb.state ${sys.usb.config}
+
+# Configfs triggers
+on property:sys.usb.config=none && property:sys.usb.configfs=1
+    write /config/usb_gadget/g1/UDC "none"
+    stop adbd
+    stop fastbootd
+    setprop sys.usb.ffs.ready 0
+    rm /config/usb_gadget/g1/configs/b.1/f1
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=sideload && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+    write /config/usb_gadget/g1/idProduct 0xD001
+    write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "adb"
+    symlink /config/usb_gadget/g1/functions/ffs.adb /config/usb_gadget/g1/configs/b.1/f1
+    write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=adb && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+    write /config/usb_gadget/g1/idProduct 0xD001
+    write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "adb"
+    symlink /config/usb_gadget/g1/functions/ffs.adb /config/usb_gadget/g1/configs/b.1/f1
+    write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+    setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=fastboot && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+    write /config/usb_gadget/g1/idProduct 0x4EE0
+    write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "fastboot"
+    symlink /config/usb_gadget/g1/functions/ffs.fastboot /config/usb_gadget/g1/configs/b.1/f1
+    write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+    setprop sys.usb.state ${sys.usb.config}
diff --git a/fastboot/fastboot.cpp b/fastboot/fastboot.cpp
new file mode 100644
index 0000000..8458c99
--- /dev/null
+++ b/fastboot/fastboot.cpp
@@ -0,0 +1,82 @@
+/*
+ * 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 "fastboot.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <bootloader_message/bootloader_message.h>
+
+#include "device.h"
+#include "ui.h"
+
+static const std::vector<std::pair<std::string, Device::BuiltinAction>> kFastbootMenuActions{
+  { "Reboot system now", Device::REBOOT },
+  { "Enter recovery", Device::ENTER_RECOVERY },
+  { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
+  { "Power off", Device::SHUTDOWN },
+};
+
+Device::BuiltinAction StartFastboot(Device* device, const std::vector<std::string>& /* args */) {
+  RecoveryUI* ui = device->GetUI();
+
+  std::vector<std::string> title_lines = { "Android Fastboot" };
+  title_lines.push_back("Product name - " + android::base::GetProperty("ro.product.device", ""));
+  title_lines.push_back("Bootloader version - " + android::base::GetProperty("ro.bootloader", ""));
+  title_lines.push_back("Baseband version - " +
+                        android::base::GetProperty("ro.build.expect.baseband", ""));
+  title_lines.push_back("Serial number - " + android::base::GetProperty("ro.serialno", ""));
+  title_lines.push_back(std::string("Secure boot - ") +
+                        ((android::base::GetProperty("ro.secure", "") == "1") ? "yes" : "no"));
+  title_lines.push_back("HW version - " + android::base::GetProperty("ro.revision", ""));
+
+  ui->ResetKeyInterruptStatus();
+  ui->SetTitle(title_lines);
+  ui->ShowText(true);
+
+  // Reset to normal system boot so recovery won't cycle indefinitely.
+  // TODO(b/112277594) Clear only if 'recovery' field of BCB is empty. If not,
+  // set the 'command' field of BCB to 'boot-recovery' so the next boot is into recovery
+  // to finish any interrupted tasks.
+  std::string err;
+  if (!clear_bootloader_message(&err)) {
+    LOG(ERROR) << "Failed to clear BCB message: " << err;
+  }
+
+  std::vector<std::string> fastboot_menu_items;
+  std::transform(kFastbootMenuActions.cbegin(), kFastbootMenuActions.cend(),
+                 std::back_inserter(fastboot_menu_items),
+                 [](const auto& entry) { return entry.first; });
+
+  auto chosen_item = ui->ShowMenu(
+      {}, fastboot_menu_items, 0, false,
+      std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+  if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+    return Device::KEY_INTERRUPTED;
+  }
+  if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT)) {
+    return Device::BuiltinAction::NO_ACTION;
+  }
+  return kFastbootMenuActions[chosen_item].second;
+}
diff --git a/mounts.h b/fastboot/fastboot.h
similarity index 66%
copy from mounts.h
copy to fastboot/fastboot.h
index 0de1ebd..53a2adc 100644
--- a/mounts.h
+++ b/fastboot/fastboot.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * 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.
@@ -14,15 +14,11 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
 
-struct MountedVolume;
+#include <string>
+#include <vector>
 
-bool scan_mounted_volumes();
+#include "device.h"
 
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+Device::BuiltinAction StartFastboot(Device* device, const std::vector<std::string>& args);
diff --git a/fsck_unshare_blocks.cpp b/fsck_unshare_blocks.cpp
new file mode 100644
index 0000000..2e6b5b8
--- /dev/null
+++ b/fsck_unshare_blocks.cpp
@@ -0,0 +1,155 @@
+/*
+ * 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 "fsck_unshare_blocks.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/unique_fd.h>
+#include <fstab/fstab.h>
+
+#include "roots.h"
+
+static constexpr const char* SYSTEM_E2FSCK_BIN = "/system/bin/e2fsck_static";
+static constexpr const char* TMP_E2FSCK_BIN = "/tmp/e2fsck.bin";
+
+static bool copy_file(const char* source, const char* dest) {
+  android::base::unique_fd source_fd(open(source, O_RDONLY));
+  if (source_fd < 0) {
+    PLOG(ERROR) << "open %s failed" << source;
+    return false;
+  }
+
+  android::base::unique_fd dest_fd(open(dest, O_CREAT | O_WRONLY, S_IRWXU));
+  if (dest_fd < 0) {
+    PLOG(ERROR) << "open %s failed" << dest;
+    return false;
+  }
+
+  for (;;) {
+    char buf[4096];
+    ssize_t rv = read(source_fd, buf, sizeof(buf));
+    if (rv < 0) {
+      PLOG(ERROR) << "read failed";
+      return false;
+    }
+    if (rv == 0) {
+      break;
+    }
+    if (write(dest_fd, buf, rv) != rv) {
+      PLOG(ERROR) << "write failed";
+      return false;
+    }
+  }
+  return true;
+}
+
+static bool run_e2fsck(const std::string& partition) {
+  Volume* volume = volume_for_mount_point(partition);
+  if (!volume) {
+    LOG(INFO) << "No fstab entry for " << partition << ", skipping.";
+    return true;
+  }
+
+  LOG(INFO) << "Running e2fsck on device " << volume->blk_device;
+
+  std::vector<std::string> args = { TMP_E2FSCK_BIN, "-p", "-E", "unshare_blocks",
+                                    volume->blk_device };
+  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;
+  char* env[] = { nullptr };
+  if (posix_spawn(&child, argv[0], nullptr, nullptr, argv.data(), env)) {
+    PLOG(ERROR) << "posix_spawn failed";
+    return false;
+  }
+
+  int status = 0;
+  int ret = TEMP_FAILURE_RETRY(waitpid(child, &status, 0));
+  if (ret < 0) {
+    PLOG(ERROR) << "waitpid failed";
+    return false;
+  }
+  if (!WIFEXITED(status)) {
+    LOG(ERROR) << "e2fsck exited abnormally: " << status;
+    return false;
+  }
+  int return_code = WEXITSTATUS(status);
+  if (return_code >= 8) {
+    LOG(ERROR) << "e2fsck could not unshare blocks: " << return_code;
+    return false;
+  }
+
+  LOG(INFO) << "Successfully unshared blocks on " << partition;
+  return true;
+}
+
+bool do_fsck_unshare_blocks() {
+  // List of partitions we will try to e2fsck -E unshare_blocks.
+  std::vector<std::string> partitions = { "/odm", "/oem", "/product", "/vendor" };
+
+  // Temporarily mount system so we can copy e2fsck_static.
+  bool mounted = false;
+  if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
+    mounted = ensure_path_mounted_at("/", "/mnt/system") != -1;
+    partitions.push_back("/");
+  } else {
+    mounted = ensure_path_mounted_at("/system", "/mnt/system") != -1;
+    partitions.push_back("/system");
+  }
+  if (!mounted) {
+    LOG(ERROR) << "Failed to mount system image.";
+    return false;
+  }
+  if (!copy_file(SYSTEM_E2FSCK_BIN, TMP_E2FSCK_BIN)) {
+    LOG(ERROR) << "Could not copy e2fsck to /tmp.";
+    return false;
+  }
+  if (umount("/mnt/system") < 0) {
+    PLOG(ERROR) << "umount failed";
+    return false;
+  }
+
+  bool ok = true;
+  for (const auto& partition : partitions) {
+    ok &= run_e2fsck(partition);
+  }
+
+  if (ok) {
+    LOG(INFO) << "Finished running e2fsck.";
+  } else {
+    LOG(ERROR) << "Finished running e2fsck, but not all partitions succceeded.";
+  }
+  return ok;
+}
diff --git a/mounts.h b/fsck_unshare_blocks.h
similarity index 67%
copy from mounts.h
copy to fsck_unshare_blocks.h
index 0de1ebd..9de8ef9 100644
--- a/mounts.h
+++ b/fsck_unshare_blocks.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * 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.
@@ -14,15 +14,9 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#ifndef _FILESYSTEM_CMDS_H
+#define _FILESYSTEM_CMDS_H
 
-struct MountedVolume;
+bool do_fsck_unshare_blocks();
 
-bool scan_mounted_volumes();
-
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+#endif  // _FILESYSTEM_CMDS_H
diff --git a/fuse_sideload/Android.bp b/fuse_sideload/Android.bp
new file mode 100644
index 0000000..90c4c22
--- /dev/null
+++ b/fuse_sideload/Android.bp
@@ -0,0 +1,38 @@
+// 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.
+
+cc_library {
+    name: "libfusesideload",
+    recovery_available: true,
+
+    cflags: [
+        "-D_XOPEN_SOURCE",
+        "-D_GNU_SOURCE",
+        "-Wall",
+        "-Werror",
+    ],
+
+    srcs: [
+        "fuse_sideload.cpp",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+    ],
+}
diff --git a/fuse_sideload.cpp b/fuse_sideload/fuse_sideload.cpp
similarity index 100%
rename from fuse_sideload.cpp
rename to fuse_sideload/fuse_sideload.cpp
diff --git a/fuse_sideload.h b/fuse_sideload/include/fuse_sideload.h
similarity index 100%
rename from fuse_sideload.h
rename to fuse_sideload/include/fuse_sideload.h
diff --git a/install.cpp b/install.cpp
index d058931..e379ef3 100644
--- a/install.cpp
+++ b/install.cpp
@@ -45,13 +45,15 @@
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
+#include <android-base/unique_fd.h>
 #include <vintf/VintfObjectRecovery.h>
 #include <ziparchive/zip_archive.h>
 
 #include "common.h"
-#include "otautil/SysUtil.h"
-#include "otautil/ThermalUtil.h"
 #include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
+#include "otautil/thermalutil.h"
 #include "private/install.h"
 #include "roots.h"
 #include "ui.h"
@@ -123,11 +125,9 @@
   }
 }
 
-#ifdef AB_OTA_UPDATER
-
-// Parses the metadata of the OTA package in |zip| and checks whether we are
-// allowed to accept this A/B package. Downgrading is not allowed unless
-// explicitly enabled in the package and only for incremental packages.
+// Parses the metadata of the OTA package in |zip| and checks whether we are allowed to accept this
+// A/B package. Downgrading is not allowed unless explicitly enabled in the package and only for
+// incremental packages.
 static int check_newer_ab_build(ZipArchiveHandle zip) {
   std::string metadata_str;
   if (!read_metadata_from_package(zip, &metadata_str)) {
@@ -213,8 +213,7 @@
   return 0;
 }
 
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
-                          const std::string& binary_path, int /* retry_count */, int status_fd,
+int SetUpAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int status_fd,
                           std::vector<std::string>* cmd) {
   CHECK(cmd != nullptr);
   int ret = check_newer_ab_build(zip);
@@ -249,7 +248,7 @@
   }
   long payload_offset = payload_entry.offset;
   *cmd = {
-    binary_path,
+    "/system/bin/update_engine_sideload",
     "--payload=file://" + package,
     android::base::StringPrintf("--offset=%ld", payload_offset),
     "--headers=" + std::string(payload_properties.begin(), payload_properties.end()),
@@ -258,14 +257,11 @@
   return 0;
 }
 
-#else  // !AB_OTA_UPDATER
-
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
-                          const std::string& binary_path, int retry_count, int status_fd,
-                          std::vector<std::string>* cmd) {
+int SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int retry_count,
+                             int status_fd, std::vector<std::string>* cmd) {
   CHECK(cmd != nullptr);
 
-  // On traditional updates we extract the update binary from the package.
+  // In non-A/B updates we extract the update binary from the package.
   static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
   ZipString binary_name(UPDATE_BINARY_NAME);
   ZipEntry binary_entry;
@@ -274,15 +270,16 @@
     return INSTALL_CORRUPT;
   }
 
+  const std::string binary_path = Paths::Get().temporary_update_binary();
   unlink(binary_path.c_str());
-  int fd = open(binary_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0755);
+  android::base::unique_fd fd(
+      open(binary_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0755));
   if (fd == -1) {
     PLOG(ERROR) << "Failed to create " << binary_path;
     return INSTALL_ERROR;
   }
 
   int32_t error = ExtractEntryToFile(zip, &binary_entry, fd);
-  close(fd);
   if (error != 0) {
     LOG(ERROR) << "Failed to extract " << UPDATE_BINARY_NAME << ": " << ErrorCodeString(error);
     return INSTALL_ERROR;
@@ -299,7 +296,6 @@
   }
   return 0;
 }
-#endif  // !AB_OTA_UPDATER
 
 static void log_max_temperature(int* max_temperature, const std::atomic<bool>& logger_finished) {
   CHECK(max_temperature != nullptr);
@@ -320,14 +316,10 @@
   int pipefd[2];
   pipe(pipefd);
 
+  bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false);
   std::vector<std::string> args;
-#ifdef AB_OTA_UPDATER
-  int ret = update_binary_command(package, zip, "/sbin/update_engine_sideload", retry_count,
-                                  pipefd[1], &args);
-#else
-  int ret = update_binary_command(package, zip, "/tmp/update-binary", retry_count, pipefd[1],
-                                  &args);
-#endif
+  int ret = is_ab ? SetUpAbUpdateCommands(package, zip, pipefd[1], &args)
+                  : SetUpNonAbUpdateCommands(package, zip, retry_count, pipefd[1], &args);
   if (ret) {
     close(pipefd[0]);
     close(pipefd[1]);
@@ -627,10 +619,8 @@
   return result;
 }
 
-int install_package(const std::string& path, bool* wipe_cache, const std::string& install_file,
-                    bool needs_mount, int retry_count) {
+int install_package(const std::string& path, bool* wipe_cache, bool needs_mount, int retry_count) {
   CHECK(!path.empty());
-  CHECK(!install_file.empty());
   CHECK(wipe_cache != nullptr);
 
   modified_flash = true;
@@ -693,6 +683,7 @@
 
   std::string log_content =
       android::base::Join(log_header, "\n") + "\n" + android::base::Join(log_buffer, "\n") + "\n";
+  const std::string& install_file = Paths::Get().temporary_install_file();
   if (!android::base::WriteStringToFile(log_content, install_file)) {
     PLOG(ERROR) << "failed to write " << install_file;
   }
diff --git a/install.h b/install.h
index f3fda30..1d3d0cd 100644
--- a/install.h
+++ b/install.h
@@ -17,16 +17,26 @@
 #ifndef RECOVERY_INSTALL_H_
 #define RECOVERY_INSTALL_H_
 
+#include <stddef.h>
+
 #include <string>
+
 #include <ziparchive/zip_archive.h>
 
-enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED,
-        INSTALL_RETRY };
+enum InstallResult {
+  INSTALL_SUCCESS,
+  INSTALL_ERROR,
+  INSTALL_CORRUPT,
+  INSTALL_NONE,
+  INSTALL_SKIPPED,
+  INSTALL_RETRY,
+  INSTALL_KEY_INTERRUPTED
+};
 
 // Installs the given update package. If INSTALL_SUCCESS is returned and *wipe_cache is true on
 // exit, caller should wipe the cache partition.
-int install_package(const std::string& package, bool* wipe_cache, const std::string& install_file,
-                    bool needs_mount, int retry_count);
+int install_package(const std::string& package, bool* wipe_cache, bool needs_mount,
+                    int retry_count);
 
 // Verify the package by ota keys. Return true if the package is verified successfully,
 // otherwise return false.
diff --git a/logging.cpp b/logging.cpp
new file mode 100644
index 0000000..50642a2
--- /dev/null
+++ b/logging.cpp
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "logging.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/klog.h>
+#include <sys/types.h>
+
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <private/android_filesystem_config.h> /* for AID_SYSTEM */
+#include <private/android_logger.h>            /* private pmsg functions */
+
+#include "common.h"
+#include "otautil/dirutil.h"
+#include "otautil/paths.h"
+#include "roots.h"
+
+static constexpr const char* LOG_FILE = "/cache/recovery/log";
+static constexpr const char* LAST_INSTALL_FILE = "/cache/recovery/last_install";
+static constexpr const char* LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+static constexpr const char* LAST_LOG_FILE = "/cache/recovery/last_log";
+
+static const std::string LAST_KMSG_FILTER = "recovery/last_kmsg";
+static const std::string LAST_LOG_FILTER = "recovery/last_log";
+
+// fopen(3)'s the given file, by mounting volumes and making parent dirs as necessary. Returns the
+// file pointer, or nullptr on error.
+static FILE* fopen_path(const std::string& path, const char* mode) {
+  if (ensure_path_mounted(path.c_str()) != 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])) {
+    mkdir_recursively(path, 0777, true, sehandle);
+  }
+  return fopen(path.c_str(), mode);
+}
+
+void check_and_fclose(FILE* fp, const std::string& name) {
+  fflush(fp);
+  if (fsync(fileno(fp)) == -1) {
+    PLOG(ERROR) << "Failed to fsync " << name;
+  }
+  if (ferror(fp)) {
+    PLOG(ERROR) << "Error in " << name;
+  }
+  fclose(fp);
+}
+
+// close a file, log an error if the error indicator is set
+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 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);
+
+  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";
+  } 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);
+    }
+  }
+
+  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;
+
+  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());
+  }
+}
+
+// Writes content to the current pmsg session.
+static ssize_t __pmsg_write(const std::string& filename, const std::string& buf) {
+  return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filename.c_str(),
+                                       buf.data(), buf.size());
+}
+
+void copy_log_file_to_pmsg(const std::string& source, const std::string& destination) {
+  std::string content;
+  android::base::ReadFileToString(source, &content);
+  __pmsg_write(destination, content);
+}
+
+// How much of the temp log we have copied to the copy in cache.
+static off_t tmplog_offset = 0;
+
+void reset_tmplog_offset() {
+  tmplog_offset = 0;
+}
+
+void copy_log_file(const std::string& source, const std::string& destination, bool append) {
+  FILE* dest_fp = fopen_path(destination, append ? "ae" : "we");
+  if (dest_fp == nullptr) {
+    PLOG(ERROR) << "Can't open " << destination;
+  } else {
+    FILE* source_fp = fopen(source.c_str(), "re");
+    if (source_fp != nullptr) {
+      if (append) {
+        fseeko(source_fp, tmplog_offset, SEEK_SET);  // Since last write
+      }
+      char buf[4096];
+      size_t bytes;
+      while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
+        fwrite(buf, 1, bytes, dest_fp);
+      }
+      if (append) {
+        tmplog_offset = ftello(source_fp);
+      }
+      check_and_fclose(source_fp, source);
+    }
+    check_and_fclose(dest_fp, destination);
+  }
+}
+
+void copy_logs(bool modified_flash, bool has_cache) {
+  // We only rotate and record the log of the current session if there are actual attempts to modify
+  // the flash, such as wipes, installs from BCB or menu selections. This is to avoid unnecessary
+  // rotation (and possible deletion) of log files, if it does not do anything loggable.
+  if (!modified_flash) {
+    return;
+  }
+
+  // Always write to pmsg, this allows the OTA logs to be caught in `logcat -L`.
+  copy_log_file_to_pmsg(Paths::Get().temporary_log_file(), LAST_LOG_FILE);
+  copy_log_file_to_pmsg(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE);
+
+  // We can do nothing for now if there's no /cache partition.
+  if (!has_cache) {
+    return;
+  }
+
+  ensure_path_mounted(LAST_LOG_FILE);
+  ensure_path_mounted(LAST_KMSG_FILE);
+  rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
+
+  // Copy logs to cache so the system can find out what happened.
+  copy_log_file(Paths::Get().temporary_log_file(), LOG_FILE, true);
+  copy_log_file(Paths::Get().temporary_log_file(), LAST_LOG_FILE, false);
+  copy_log_file(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE, false);
+  save_kernel_log(LAST_KMSG_FILE);
+  chmod(LOG_FILE, 0600);
+  chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
+  chmod(LAST_KMSG_FILE, 0600);
+  chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
+  chmod(LAST_LOG_FILE, 0640);
+  chmod(LAST_INSTALL_FILE, 0644);
+  chown(LAST_INSTALL_FILE, AID_SYSTEM, AID_SYSTEM);
+  sync();
+}
+
+// Read from kernel log into buffer and write out to file.
+void save_kernel_log(const char* destination) {
+  int klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
+  if (klog_buf_len <= 0) {
+    PLOG(ERROR) << "Error getting klog size";
+    return;
+  }
+
+  std::string buffer(klog_buf_len, 0);
+  int n = klogctl(KLOG_READ_ALL, &buffer[0], klog_buf_len);
+  if (n == -1) {
+    PLOG(ERROR) << "Error in reading klog";
+    return;
+  }
+  buffer.resize(n);
+  android::base::WriteStringToFile(buffer, destination);
+}
diff --git a/rotate_logs.h b/logging.h
similarity index 70%
rename from rotate_logs.h
rename to logging.h
index 007c33d..3cfbc7a 100644
--- a/rotate_logs.h
+++ b/logging.h
@@ -14,12 +14,14 @@
  * limitations under the License.
  */
 
-#ifndef _ROTATE_LOGS_H
-#define _ROTATE_LOGS_H
+#ifndef _LOGGING_H
+#define _LOGGING_H
 
 #include <stddef.h>
 #include <sys/types.h>
 
+#include <string>
+
 #include <log/log_id.h>
 
 static constexpr int KEEP_LOG_COUNT = 10;
@@ -35,4 +37,14 @@
 // Overwrite any existing last_log.$max and last_kmsg.$max.
 void rotate_logs(const char* last_log_file, const char* last_kmsg_file);
 
-#endif //_ROTATE_LOG_H
+// In turn fflush(3)'s, fsync(3)'s and fclose(3)'s the given stream.
+void check_and_fclose(FILE* fp, const std::string& name);
+
+void copy_log_file_to_pmsg(const std::string& source, const std::string& destination);
+void copy_log_file(const std::string& source, const std::string& destination, bool append);
+void copy_logs(bool modified_flash, bool has_cache);
+void reset_tmplog_offset();
+
+void save_kernel_log(const char* destination);
+
+#endif  //_LOGGING_H
diff --git a/minadbd/Android.bp b/minadbd/Android.bp
new file mode 100644
index 0000000..370232b
--- /dev/null
+++ b/minadbd/Android.bp
@@ -0,0 +1,79 @@
+// 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.
+
+cc_defaults {
+    name: "minadbd_defaults",
+
+    cflags: [
+        "-DADB_HOST=0",
+        "-Wall",
+        "-Werror",
+    ],
+
+    include_dirs: [
+        "system/core/adb",
+    ],
+}
+
+// `libminadbd_services` is analogous to the `libadbd_services` for regular `adbd`, but providing
+// the sideload service only.
+cc_library {
+    name: "libminadbd_services",
+    recovery_available: true,
+
+    defaults: [
+        "minadbd_defaults",
+    ],
+
+    srcs: [
+        "fuse_adb_provider.cpp",
+        "minadbd.cpp",
+        "minadbd_services.cpp",
+    ],
+
+    shared_libs: [
+        "libadbd",
+        "libbase",
+        "libcrypto",
+        "libfusesideload",
+    ],
+}
+
+cc_test {
+    name: "minadbd_test",
+    isolated: true,
+
+    defaults: [
+        "minadbd_defaults",
+    ],
+
+    srcs: [
+        "fuse_adb_provider_test.cpp",
+    ],
+
+    static_libs: [
+        "libminadbd_services",
+        "libadbd",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libcutils",
+        "liblog",
+    ],
+
+    test_suites: [
+        "device-tests",
+    ],
+}
diff --git a/minadbd/Android.mk b/minadbd/Android.mk
deleted file mode 100644
index 50e3b34..0000000
--- a/minadbd/Android.mk
+++ /dev/null
@@ -1,55 +0,0 @@
-# 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 \
-    -DADB_HOST=0 \
-
-# libminadbd (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    fuse_adb_provider.cpp \
-    minadbd.cpp \
-    minadbd_services.cpp \
-
-LOCAL_MODULE := libminadbd
-LOCAL_CFLAGS := $(minadbd_cflags)
-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_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 := \
-    libBionicGtestMain \
-    libminadbd
-LOCAL_SHARED_LIBRARIES := \
-    liblog \
-    libbase \
-    libcutils
-
-include $(BUILD_NATIVE_TEST)
diff --git a/minadbd/minadbd_services.cpp b/minadbd/minadbd_services.cpp
index 043c51a..e9c51da 100644
--- a/minadbd/minadbd_services.cpp
+++ b/minadbd/minadbd_services.cpp
@@ -21,46 +21,35 @@
 #include <string.h>
 #include <unistd.h>
 
+#include <functional>
 #include <string>
 #include <thread>
 
 #include "adb.h"
+#include "adb_unique_fd.h"
 #include "fdevent.h"
 #include "fuse_adb_provider.h"
+#include "services.h"
 #include "sysdeps.h"
 
-static void sideload_host_service(int sfd, const std::string& args) {
-    int file_size;
-    int block_size;
-    if (sscanf(args.c_str(), "%d:%d", &file_size, &block_size) != 2) {
-        printf("bad sideload-host arguments: %s\n", args.c_str());
-        exit(1);
-    }
+static void sideload_host_service(unique_fd sfd, const std::string& args) {
+  int64_t file_size;
+  int block_size;
+  if ((sscanf(args.c_str(), "%" SCNd64 ":%d", &file_size, &block_size) != 2) || file_size <= 0 ||
+      block_size <= 0) {
+    printf("bad sideload-host arguments: %s\n", args.c_str());
+    exit(1);
+  }
 
-    printf("sideload-host file size %d block size %d\n", file_size, block_size);
+  printf("sideload-host file size %" PRId64 " block size %d\n", file_size, block_size);
 
-    int result = run_adb_fuse(sfd, file_size, block_size);
+  int result = run_adb_fuse(sfd, file_size, block_size);
 
-    printf("sideload_host finished\n");
-    exit(result == 0 ? 0 : 1);
+  printf("sideload_host finished\n");
+  exit(result == 0 ? 0 : 1);
 }
 
-static int create_service_thread(void (*func)(int, const std::string&), const std::string& args) {
-    int s[2];
-    if (adb_socketpair(s)) {
-        printf("cannot create service socket pair\n");
-        return -1;
-    }
-
-    std::thread([s, func, args]() { func(s[1], args); }).detach();
-
-    VLOG(SERVICES) << "service thread started, " << s[0] << ":" << s[1];
-    return s[0];
-}
-
-int service_to_fd(const char* name, atransport* /* transport */) {
-  int ret = -1;
-
+unique_fd daemon_service_to_fd(const char* name, atransport* /* transport */) {
   if (!strncmp(name, "sideload:", 9)) {
     // this exit status causes recovery to print a special error
     // message saying to use a newer adb (that supports
@@ -68,10 +57,8 @@
     exit(3);
   } else if (!strncmp(name, "sideload-host:", 14)) {
     std::string arg(name + 14);
-    ret = create_service_thread(sideload_host_service, arg);
+    return create_service_thread("sideload-host",
+                                 std::bind(sideload_host_service, std::placeholders::_1, arg));
   }
-  if (ret >= 0) {
-    close_on_exec(ret);
-  }
-  return ret;
+  return unique_fd{};
 }
diff --git a/minui/Android.bp b/minui/Android.bp
new file mode 100644
index 0000000..fff3a8e
--- /dev/null
+++ b/minui/Android.bp
@@ -0,0 +1,47 @@
+// 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.
+
+cc_library {
+    name: "libminui",
+    recovery_available: true,
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    srcs: [
+        "events.cpp",
+        "graphics.cpp",
+        "graphics_adf.cpp",
+        "graphics_drm.cpp",
+        "graphics_fbdev.cpp",
+        "resources.cpp",
+    ],
+
+    whole_static_libs: [
+        "libadf",
+        "libdrm",
+        "libsync",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libpng",
+        "libz",
+    ],
+}
diff --git a/minui/Android.mk b/minui/Android.mk
deleted file mode 100644
index ae1552b..0000000
--- a/minui/Android.mk
+++ /dev/null
@@ -1,85 +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.
-
-LOCAL_PATH := $(call my-dir)
-
-# libminui (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    events.cpp \
-    graphics.cpp \
-    graphics_adf.cpp \
-    graphics_drm.cpp \
-    graphics_fbdev.cpp \
-    resources.cpp \
-
-LOCAL_WHOLE_STATIC_LIBRARIES := \
-    libadf \
-    libdrm \
-    libsync_recovery
-
-LOCAL_STATIC_LIBRARIES := \
-    libpng \
-    libbase
-
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-
-LOCAL_MODULE := libminui
-
-# This used to compare against values in double-quotes (which are just
-# ordinary characters in this context).  Strip double-quotes from the
-# value so that either will work.
-
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),ABGR_8888)
-  LOCAL_CFLAGS += -DRECOVERY_ABGR
-endif
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),RGBX_8888)
-  LOCAL_CFLAGS += -DRECOVERY_RGBX
-endif
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),BGRA_8888)
-  LOCAL_CFLAGS += -DRECOVERY_BGRA
-endif
-
-ifneq ($(TARGET_RECOVERY_OVERSCAN_PERCENT),)
-  LOCAL_CFLAGS += -DOVERSCAN_PERCENT=$(TARGET_RECOVERY_OVERSCAN_PERCENT)
-else
-  LOCAL_CFLAGS += -DOVERSCAN_PERCENT=0
-endif
-
-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
-LOCAL_WHOLE_STATIC_LIBRARIES += libminui
-LOCAL_SHARED_LIBRARIES := \
-    libpng \
-    libbase
-
-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/font_10x18.h b/minui/font_10x18.h
deleted file mode 100644
index 30dfb9c..0000000
--- a/minui/font_10x18.h
+++ /dev/null
@@ -1,214 +0,0 @@
-struct {
-  unsigned width;
-  unsigned height;
-  unsigned char_width;
-  unsigned char_height;
-  unsigned char rundata[2973];
-} font = {
-  .width = 960,
-  .height = 18,
-  .char_width = 10,
-  .char_height = 18,
-  .rundata = {
-0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x55,0x82,0x06,0x82,0x02,0x82,0x10,0x82,
-0x11,0x83,0x08,0x82,0x0a,0x82,0x04,0x82,0x46,0x82,0x08,0x82,0x07,0x84,0x06,
-0x84,0x0a,0x81,0x03,0x88,0x04,0x84,0x04,0x88,0x04,0x84,0x06,0x84,0x1e,0x81,
-0x0e,0x81,0x0a,0x84,0x06,0x84,0x07,0x82,0x05,0x85,0x07,0x84,0x04,0x86,0x04,
-0x88,0x02,0x88,0x04,0x84,0x04,0x82,0x04,0x82,0x02,0x88,0x05,0x86,0x01,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,
-0x86,0x06,0x84,0x04,0x86,0x06,0x84,0x04,0x88,0x02,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
-0x88,0x03,0x86,0x0e,0x86,0x06,0x82,0x11,0x82,0x10,0x82,0x18,0x82,0x0f,0x84,
-0x0d,0x82,0x1c,0x82,0x09,0x84,0x7f,0x16,0x84,0x05,0x82,0x05,0x84,0x07,0x83,
-0x02,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x03,0x86,0x04,
-0x83,0x02,0x82,0x03,0x82,0x01,0x82,0x07,0x82,0x09,0x82,0x06,0x82,0x3e,0x82,
-0x04,0x84,0x06,0x83,0x06,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x03,
-0x82,0x09,0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
-0x1c,0x82,0x0e,0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x05,0x84,0x04,
-0x82,0x02,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
-0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x04,
-0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x02,
-0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x03,0x82,0x08,0x82,0x0c,
-0x82,0x05,0x84,0x11,0x82,0x0f,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,
-0x1c,0x82,0x0b,0x82,0x7f,0x15,0x82,0x08,0x82,0x08,0x82,0x05,0x82,0x01,0x82,
-0x01,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x02,0x82,0x01,
-0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x07,0x82,
-0x08,0x82,0x08,0x82,0x3d,0x82,0x03,0x82,0x02,0x82,0x04,0x84,0x05,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x06,0x83,0x03,0x82,0x08,0x82,0x04,0x81,0x09,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x1a,0x82,0x10,0x82,0x06,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,
-0x02,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x08,0x82,0x0c,0x82,0x04,0x82,0x02,0x82,
-0x11,0x82,0x0e,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,0x0b,0x82,0x0b,
-0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x05,0x82,
-0x02,0x83,0x1a,0x82,0x07,0x81,0x02,0x81,0x07,0x82,0x01,0x82,0x02,0x82,0x01,
-0x82,0x05,0x82,0x01,0x84,0x04,0x82,0x01,0x82,0x07,0x82,0x08,0x82,0x08,0x82,
-0x06,0x82,0x02,0x82,0x06,0x82,0x28,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,
-0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x84,0x03,0x82,0x08,0x82,
-0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x19,0x82,0x12,0x82,0x05,
-0x82,0x04,0x82,0x02,0x82,0x02,0x84,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
-0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,
-0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,0x02,0x83,
-0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,0x05,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
-0x82,0x04,0x82,0x09,0x82,0x0b,0x82,0x03,0x82,0x04,0x82,0x20,0x82,0x18,0x82,
-0x0e,0x82,0x10,0x82,0x0b,0x82,0x0b,0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,
-0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x88,0x01,0x82,0x01,0x82,0x06,0x83,
-0x01,0x82,0x04,0x84,0x08,0x81,0x08,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,
-0x82,0x28,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x08,0x82,0x04,0x82,
-0x01,0x82,0x03,0x82,0x08,0x82,0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x18,0x82,0x06,0x88,0x06,0x82,0x04,0x82,0x04,0x82,0x02,0x82,0x01,0x85,
-0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,
-0x02,0x82,0x04,0x82,0x08,0x88,0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,
-0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x84,0x06,
-0x84,0x08,0x82,0x05,0x82,0x09,0x82,0x0b,0x82,0x2b,0x82,0x18,0x82,0x0e,0x82,
-0x10,0x82,0x1c,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x11,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x09,0x82,0x06,0x82,0x12,0x82,
-0x0a,0x82,0x06,0x84,0x07,0x82,0x27,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,
-0x82,0x07,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,0x83,0x04,0x82,0x01,0x83,
-0x08,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x83,0x07,0x83,0x05,
-0x82,0x16,0x82,0x08,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,
-0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,
-0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
-0x0a,0x82,0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,
-0x82,0x04,0x84,0x06,0x84,0x08,0x82,0x05,0x82,0x0a,0x82,0x0a,0x82,0x23,0x85,
-0x03,0x82,0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x84,0x04,0x86,0x05,
-0x85,0x01,0x81,0x02,0x82,0x01,0x83,0x05,0x84,0x09,0x84,0x02,0x82,0x03,0x82,
-0x06,0x82,0x05,0x81,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x83,0x06,0x84,0x04,
-0x82,0x01,0x83,0x06,0x83,0x01,0x82,0x02,0x82,0x01,0x84,0x04,0x86,0x03,0x86,
-0x04,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x87,0x05,0x82,0x08,0x82,0x08,0x82,0x26,0x82,
-0x11,0x82,0x01,0x82,0x04,0x86,0x07,0x82,0x05,0x83,0x12,0x82,0x0a,0x82,0x04,
-0x88,0x02,0x88,0x0c,0x88,0x10,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,
-0x06,0x83,0x04,0x82,0x03,0x82,0x03,0x83,0x02,0x82,0x03,0x83,0x02,0x82,0x07,
-0x82,0x06,0x84,0x05,0x82,0x02,0x83,0x05,0x83,0x07,0x83,0x04,0x82,0x18,0x82,
-0x06,0x82,0x04,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x86,0x04,
-0x82,0x08,0x82,0x04,0x82,0x02,0x86,0x04,0x86,0x04,0x82,0x02,0x84,0x02,0x88,
-0x05,0x82,0x0a,0x82,0x03,0x85,0x05,0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,
-0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x02,0x82,0x03,0x82,0x05,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x03,
-0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x08,0x82,0x08,0x82,
-0x06,0x82,0x0a,0x82,0x0a,0x82,0x22,0x82,0x03,0x82,0x02,0x83,0x02,0x82,0x04,
-0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x02,0x82,0x05,0x82,0x06,0x82,
-0x03,0x83,0x02,0x83,0x02,0x82,0x06,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,0x07,
-0x82,0x05,0x88,0x02,0x83,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,
-0x04,0x82,0x02,0x83,0x03,0x83,0x02,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x04,0x82,0x08,0x82,0x02,0x84,0x09,0x82,0x09,0x84,0x23,0x82,0x11,
-0x82,0x01,0x82,0x06,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x01,0x82,0x11,0x82,
-0x0a,0x82,0x06,0x84,0x07,0x82,0x26,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x08,
-0x83,0x09,0x82,0x03,0x82,0x03,0x82,0x09,0x82,0x02,0x82,0x04,0x82,0x05,0x82,
-0x06,0x82,0x02,0x82,0x05,0x83,0x01,0x82,0x17,0x82,0x16,0x82,0x06,0x82,0x05,
-0x82,0x01,0x82,0x01,0x82,0x02,0x88,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
-0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x86,0x04,0x82,0x04,0x82,0x02,
-0x86,0x09,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,
-0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,0x82,0x27,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
-0x82,0x01,0x82,0x08,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x07,
-0x82,0x0a,0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,
-0x04,0x84,0x04,0x82,0x04,0x82,0x07,0x82,0x06,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x0f,0x88,0x05,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x02,0x82,0x01,0x82,
-0x0d,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,0x82,0x26,0x82,0x05,0x82,0x04,
-0x82,0x05,0x82,0x07,0x82,0x0c,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,
-0x05,0x82,0x05,0x82,0x04,0x82,0x08,0x82,0x18,0x82,0x14,0x82,0x07,0x82,0x05,
-0x82,0x01,0x84,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
-0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x02,0x82,0x0a,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,
-0x01,0x82,0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,
-0x82,0x22,0x87,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x88,
-0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
-0x84,0x09,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x08,0x86,0x05,
-0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,
-0x05,0x82,0x05,0x82,0x04,0x82,0x06,0x82,0x07,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x10,0x82,0x01,0x82,0x07,0x82,0x01,0x82,0x04,0x82,0x01,0x83,0x02,0x82,
-0x03,0x83,0x0f,0x82,0x08,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x25,0x82,0x07,
-0x82,0x02,0x82,0x06,0x82,0x06,0x82,0x07,0x82,0x04,0x82,0x07,0x82,0x09,0x82,
-0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,0x82,0x08,0x82,0x19,0x82,0x05,
-0x88,0x05,0x82,0x08,0x82,0x05,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
-0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,
-0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x03,0x82,0x09,0x82,0x05,0x82,
-0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x06,
-0x82,0x06,0x82,0x08,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,0x82,0x06,0x82,0x03,
-0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x85,0x08,0x82,0x05,0x82,
-0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,0x06,0x82,0x04,0x82,
-0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,
-0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x38,0x82,0x01,0x82,0x04,0x82,0x01,0x82,
-0x01,0x82,0x04,0x84,0x01,0x82,0x01,0x82,0x03,0x82,0x10,0x82,0x08,0x82,0x30,
-0x83,0x06,0x82,0x07,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x08,0x82,0x04,0x82,
-0x07,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,
-0x82,0x03,0x81,0x04,0x82,0x1a,0x82,0x10,0x82,0x10,0x82,0x08,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
-0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,
-0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,
-0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x02,0x84,0x02,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x05,0x83,0x02,0x83,0x03,
-0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x09,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,
-0x82,0x07,0x85,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,
-0x07,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,
-0x06,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x04,0x84,0x04,
-0x82,0x04,0x82,0x04,0x82,0x09,0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x82,
-0x01,0x82,0x05,0x86,0x04,0x82,0x01,0x82,0x01,0x82,0x01,0x83,0x01,0x84,0x10,
-0x82,0x06,0x82,0x1d,0x83,0x11,0x83,0x05,0x82,0x09,0x84,0x07,0x82,0x05,0x82,
-0x09,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x06,0x83,0x07,0x83,0x09,0x82,
-0x0e,0x82,0x0a,0x82,0x06,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,
-0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x09,0x82,
-0x02,0x83,0x02,0x82,0x04,0x82,0x05,0x82,0x06,0x82,0x01,0x82,0x04,0x82,0x04,
-0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x06,
-0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x05,0x82,0x05,0x82,0x09,0x82,0x0d,0x82,0x07,0x82,0x21,0x82,0x04,0x82,0x02,
-0x83,0x02,0x82,0x04,0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x03,0x82,
-0x04,0x82,0x06,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x03,
-0x82,0x06,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
-0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x07,0x82,0x04,
-0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x02,0x83,0x05,0x82,0x05,0x88,0x03,0x82,
-0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x0a,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x1c,0x82,0x06,0x82,0x02,0x83,0x03,0x84,0x02,0x82,0x10,0x82,0x04,0x82,
-0x1e,0x83,0x11,0x83,0x05,0x82,0x0a,0x82,0x05,0x88,0x02,0x88,0x04,0x84,0x09,
-0x82,0x05,0x84,0x06,0x84,0x05,0x82,0x09,0x84,0x06,0x84,0x07,0x83,0x07,0x83,
-0x0a,0x81,0x0e,0x81,0x0b,0x82,0x07,0x85,0x03,0x82,0x04,0x82,0x02,0x86,0x06,
-0x84,0x04,0x86,0x04,0x88,0x02,0x82,0x0a,0x84,0x01,0x81,0x02,0x82,0x04,0x82,
-0x02,0x88,0x04,0x83,0x05,0x82,0x04,0x82,0x02,0x88,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x0a,0x85,0x03,0x82,0x04,0x82,0x04,0x84,
-0x07,0x82,0x07,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x05,0x88,0x03,0x86,0x09,0x82,0x03,0x86,0x22,0x85,0x01,0x81,0x02,0x82,
-0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x85,0x05,0x82,0x07,0x86,0x03,
-0x82,0x04,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x88,0x02,0x82,
-0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x83,0x06,
-0x83,0x01,0x82,0x03,0x82,0x08,0x86,0x06,0x84,0x05,0x83,0x01,0x82,0x05,0x82,
-0x06,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x83,0x01,0x82,0x03,0x87,0x06,
-0x84,0x05,0x82,0x05,0x84,0x7f,0x15,0x83,0x7f,0x14,0x83,0x7f,0x5e,0x82,0x7f,
-0x05,0x89,0x47,0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x4e,
-0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,0x82,0x04,0x82,0x17,0x82,0x03,0x82,
-0x34,0x82,0x0e,0x82,0x48,0x82,0x04,0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,
-0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x49,0x82,0x02,0x82,
-0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0c,0x86,0x19,0x85,0x35,0x82,0x0e,0x82,0x4a,
-0x84,0x3f,
-0x00,
-  }
-};
diff --git a/minui/graphics.cpp b/minui/graphics.cpp
index 56f471b..e6367d9 100644
--- a/minui/graphics.cpp
+++ b/minui/graphics.cpp
@@ -23,41 +23,57 @@
 
 #include <memory>
 
-#include "font_10x18.h"
+#include <android-base/properties.h>
+
 #include "graphics_adf.h"
 #include "graphics_drm.h"
 #include "graphics_fbdev.h"
 #include "minui/minui.h"
 
-static GRFont* gr_font = NULL;
+static GRFont* gr_font = nullptr;
 static MinuiBackend* gr_backend = nullptr;
 
-static int overscan_percent = OVERSCAN_PERCENT;
 static int overscan_offset_x = 0;
 static int overscan_offset_y = 0;
 
 static uint32_t gr_current = ~0;
 static constexpr uint32_t alpha_mask = 0xff000000;
 
-static GRSurface* gr_draw = NULL;
-static GRRotation rotation = ROTATION_NONE;
+// gr_draw is owned by backends.
+static const GRSurface* gr_draw = nullptr;
+static GRRotation rotation = GRRotation::NONE;
+static PixelFormat pixel_format = PixelFormat::UNKNOWN;
 
 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);
+  auto swapped = (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT);
+  return x < 0 || x >= (swapped ? gr_draw->height : gr_draw->width) || y < 0 ||
+         y >= (swapped ? gr_draw->width : gr_draw->height);
 }
 
 const GRFont* gr_sys_font() {
   return gr_font;
 }
 
+PixelFormat gr_pixel_format() {
+  return pixel_format;
+}
+
 int gr_measure(const GRFont* font, const char* s) {
+  if (font == nullptr) {
+    return -1;
+  }
+
   return font->char_width * strlen(s);
 }
 
-void gr_font_size(const GRFont* font, int* x, int* y) {
+int gr_font_size(const GRFont* font, int* x, int* y) {
+  if (font == nullptr) {
+    return -1;
+  }
+
   *x = font->char_width;
   *y = font->char_height;
+  return 0;
 }
 
 // Blends gr_current onto pix value, assumes alpha as most significant byte.
@@ -78,38 +94,46 @@
   return (out_r & 0xff) | (out_g & 0xff00) | (out_b & 0xff0000) | (gr_current & 0xff000000);
 }
 
-// increments pixel pointer right, with current rotation.
+// 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);
+  if (rotation == GRRotation::LEFT) {
+    *p = *p - row_pixels;
+  } else if (rotation == GRRotation::RIGHT) {
+    *p = *p + row_pixels;
+  } else if (rotation == GRRotation::DOWN) {
+    *p = *p - 1;
+  } else {  // GRRotation::NONE
+    *p = *p + 1;
   }
 }
 
-// increments pixel pointer down, with current rotation.
+// 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;
+  if (rotation == GRRotation::LEFT) {
+    *p = *p + 1;
+  } else if (rotation == GRRotation::RIGHT) {
+    *p = *p - 1;
+  } else if (rotation == GRRotation::DOWN) {
+    *p = *p - row_pixels;
+  } else {  // GRRotation::NONE
+    *p = *p + 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) {
+// Returns pixel pointer at given coordinates with rotation adjustment.
+static uint32_t* pixel_at(const GRSurface* surf, int x, int y, int row_pixels) {
   switch (rotation) {
-    case ROTATION_NONE:
+    case GRRotation::NONE:
       return reinterpret_cast<uint32_t*>(surf->data) + y * row_pixels + x;
-    case ROTATION_RIGHT:
+    case GRRotation::RIGHT:
       return reinterpret_cast<uint32_t*>(surf->data) + x * row_pixels + (surf->width - y);
-    case ROTATION_DOWN:
+    case GRRotation::DOWN:
       return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - y) * row_pixels +
              (surf->width - 1 - x);
-    case ROTATION_LEFT:
+    case GRRotation::LEFT:
       return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - x) * row_pixels + y;
     default:
-      printf("invalid rotation %d", rotation);
+      printf("invalid rotation %d", static_cast<int>(rotation));
   }
   return nullptr;
 }
@@ -164,7 +188,7 @@
 }
 
 void gr_texticon(int x, int y, GRSurface* icon) {
-  if (icon == NULL) return;
+  if (icon == nullptr) return;
 
   if (icon->pixel_bytes != 1) {
     printf("gr_texticon: source has wrong format\n");
@@ -185,11 +209,11 @@
 
 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 = (a32 << 24) | (r32 << 16) | (g32 << 8) | b32;
-#else
-  gr_current = (a32 << 24) | (b32 << 16) | (g32 << 8) | r32;
-#endif
+  if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+    gr_current = (a32 << 24) | (r32 << 16) | (g32 << 8) | b32;
+  } else {
+    gr_current = (a32 << 24) | (b32 << 16) | (g32 << 8) | r32;
+  }
 }
 
 void gr_clear() {
@@ -235,7 +259,7 @@
 }
 
 void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
-  if (source == NULL) return;
+  if (source == nullptr) return;
 
   if (gr_draw->pixel_bytes != source->pixel_bytes) {
     printf("gr_blit: source has wrong format\n");
@@ -247,7 +271,7 @@
 
   if (outside(dx, dy) || outside(dx + w - 1, dy + h - 1)) return;
 
-  if (rotation) {
+  if (rotation != GRRotation::NONE) {
     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;
@@ -267,8 +291,7 @@
     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) {
+    for (int 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;
@@ -276,15 +299,15 @@
   }
 }
 
-unsigned int gr_get_width(GRSurface* surface) {
-  if (surface == NULL) {
+unsigned int gr_get_width(const GRSurface* surface) {
+  if (surface == nullptr) {
     return 0;
   }
   return surface->width;
 }
 
-unsigned int gr_get_height(GRSurface* surface) {
-  if (surface == NULL) {
+unsigned int gr_get_height(const GRSurface* surface) {
+  if (surface == nullptr) {
     return 0;
   }
   return surface->height;
@@ -313,42 +336,28 @@
   return 0;
 }
 
-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);
-
-  // 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 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;
-}
-
 void gr_flip() {
   gr_draw = gr_backend->Flip();
 }
 
 int gr_init() {
-  gr_init_font();
+  // pixel_format needs to be set before loading any resources or initializing backends.
+  std::string format = android::base::GetProperty("ro.minui.pixel_format", "");
+  if (format == "ABGR_8888") {
+    pixel_format = PixelFormat::ABGR;
+  } else if (format == "RGBX_8888") {
+    pixel_format = PixelFormat::RGBX;
+  } else if (format == "BGRA_8888") {
+    pixel_format = PixelFormat::BGRA;
+  } else {
+    pixel_format = PixelFormat::UNKNOWN;
+  }
+
+  int ret = gr_init_font("font", &gr_font);
+  if (ret != 0) {
+    printf("Failed to init font: %d, continuing graphic backend initialization without font file\n",
+           ret);
+  }
 
   auto backend = std::unique_ptr<MinuiBackend>{ std::make_unique<MinuiBackendAdf>() };
   gr_draw = backend->Init();
@@ -369,13 +378,28 @@
 
   gr_backend = backend.release();
 
+  int overscan_percent = android::base::GetIntProperty("ro.minui.overscan_percent", 0);
   overscan_offset_x = gr_draw->width * overscan_percent / 100;
   overscan_offset_y = gr_draw->height * overscan_percent / 100;
 
   gr_flip();
   gr_flip();
+  if (!gr_draw) {
+    printf("gr_init: gr_draw becomes nullptr after gr_flip\n");
+    return -1;
+  }
 
-  gr_rotate(DEFAULT_ROTATION);
+  std::string rotation_str =
+      android::base::GetProperty("ro.minui.default_rotation", "ROTATION_NONE");
+  if (rotation_str == "ROTATION_RIGHT") {
+    gr_rotate(GRRotation::RIGHT);
+  } else if (rotation_str == "ROTATION_DOWN") {
+    gr_rotate(GRRotation::DOWN);
+  } else if (rotation_str == "ROTATION_LEFT") {
+    gr_rotate(GRRotation::LEFT);
+  } else {  // "ROTATION_NONE" or unknown string
+    gr_rotate(GRRotation::NONE);
+  }
 
   if (gr_draw->pixel_bytes != 4) {
     printf("gr_init: Only 4-byte pixel formats supported\n");
@@ -386,16 +410,22 @@
 
 void gr_exit() {
   delete gr_backend;
+  gr_backend = nullptr;
+
+  delete gr_font;
+  gr_font = nullptr;
 }
 
 int gr_fb_width() {
-  return rotation % 2 ? gr_draw->height - 2 * overscan_offset_y
-                      : gr_draw->width - 2 * overscan_offset_x;
+  return (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT)
+             ? gr_draw->height - 2 * overscan_offset_y
+             : gr_draw->width - 2 * overscan_offset_x;
 }
 
 int gr_fb_height() {
-  return rotation % 2 ? gr_draw->width - 2 * overscan_offset_x
-                      : gr_draw->height - 2 * overscan_offset_y;
+  return (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT)
+             ? gr_draw->width - 2 * overscan_offset_x
+             : gr_draw->height - 2 * overscan_offset_y;
 }
 
 void gr_fb_blank(bool blank) {
diff --git a/minui/graphics_adf.cpp b/minui/graphics_adf.cpp
index a59df00..7439df9 100644
--- a/minui/graphics_adf.cpp
+++ b/minui/graphics_adf.cpp
@@ -104,15 +104,16 @@
 }
 
 GRSurface* MinuiBackendAdf::Init() {
-#if defined(RECOVERY_ABGR)
-  format = DRM_FORMAT_ABGR8888;
-#elif defined(RECOVERY_BGRA)
-  format = DRM_FORMAT_BGRA8888;
-#elif defined(RECOVERY_RGBX)
-  format = DRM_FORMAT_RGBX8888;
-#else
-  format = DRM_FORMAT_RGB565;
-#endif
+  PixelFormat pixel_format = gr_pixel_format();
+  if (pixel_format == PixelFormat::ABGR) {
+    format = DRM_FORMAT_ABGR8888;
+  } else if (pixel_format == PixelFormat::BGRA) {
+    format = DRM_FORMAT_BGRA8888;
+  } else if (pixel_format == PixelFormat::RGBX) {
+    format = DRM_FORMAT_RGBX8888;
+  } else {
+    format = DRM_FORMAT_RGB565;
+  }
 
   adf_id_t* dev_ids = nullptr;
   ssize_t n_dev_ids = adf_devices(&dev_ids);
diff --git a/minui/graphics_drm.cpp b/minui/graphics_drm.cpp
index e7d4b38..630b801 100644
--- a/minui/graphics_drm.cpp
+++ b/minui/graphics_drm.cpp
@@ -17,6 +17,7 @@
 #include "graphics_drm.h"
 
 #include <fcntl.h>
+#include <poll.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/mman.h>
@@ -45,15 +46,17 @@
   }
 }
 
-void MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface) {
-  int32_t ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0,  // x,y
-                               &main_monitor_connector->connector_id,
-                               1,  // connector_count
-                               &main_monitor_crtc->mode);
+int MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface) {
+  int ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0,  // x,y
+                           &main_monitor_connector->connector_id,
+                           1,  // connector_count
+                           &main_monitor_crtc->mode);
 
   if (ret) {
     printf("drmModeSetCrtc failed ret=%d\n", ret);
   }
+
+  return ret;
 }
 
 void MinuiBackendDrm::Blank(bool blank) {
@@ -113,15 +116,20 @@
   *surface = {};
 
   uint32_t format;
-#if defined(RECOVERY_ABGR)
-  format = DRM_FORMAT_RGBA8888;
-#elif defined(RECOVERY_BGRA)
-  format = DRM_FORMAT_ARGB8888;
-#elif defined(RECOVERY_RGBX)
-  format = DRM_FORMAT_XBGR8888;
-#else
-  format = DRM_FORMAT_RGB565;
-#endif
+  PixelFormat pixel_format = gr_pixel_format();
+  // PixelFormat comes in byte order, whereas DRM_FORMAT_* uses little-endian
+  // (external/libdrm/include/drm/drm_fourcc.h). Note that although drm_fourcc.h also defines a
+  // macro of DRM_FORMAT_BIG_ENDIAN, it doesn't seem to be actually supported (see the discussion
+  // in https://lists.freedesktop.org/archives/amd-gfx/2017-May/008560.html).
+  if (pixel_format == PixelFormat::ABGR) {
+    format = DRM_FORMAT_RGBA8888;
+  } else if (pixel_format == PixelFormat::BGRA) {
+    format = DRM_FORMAT_ARGB8888;
+  } else if (pixel_format == PixelFormat::RGBX) {
+    format = DRM_FORMAT_XBGR8888;
+  } else {
+    format = DRM_FORMAT_RGB565;
+  }
 
   drm_mode_create_dumb create_dumb = {};
   create_dumb.height = height;
@@ -368,18 +376,57 @@
 
   current_buffer = 0;
 
-  DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]);
+  // We will likely encounter errors in the backend functions (i.e. Flip) if EnableCrtc fails.
+  if (DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]) != 0) {
+    return nullptr;
+  }
 
   return GRSurfaceDrms[0];
 }
 
+static void page_flip_complete(__unused int fd,
+                               __unused unsigned int sequence,
+                               __unused unsigned int tv_sec,
+                               __unused unsigned int tv_usec,
+                               void *user_data) {
+  *static_cast<bool*>(user_data) = false;
+}
+
 GRSurface* MinuiBackendDrm::Flip() {
+  bool ongoing_flip = true;
+
   int ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id,
-                            GRSurfaceDrms[current_buffer]->fb_id, 0, nullptr);
+                            GRSurfaceDrms[current_buffer]->fb_id,
+                            DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip);
   if (ret < 0) {
     printf("drmModePageFlip failed ret=%d\n", ret);
     return nullptr;
   }
+
+  while (ongoing_flip) {
+    struct pollfd fds = {
+      .fd = drm_fd,
+      .events = POLLIN
+    };
+
+    ret = poll(&fds, 1, -1);
+    if (ret == -1 || !(fds.revents & POLLIN)) {
+      printf("poll() failed on drm fd\n");
+      break;
+    }
+
+    drmEventContext evctx = {
+      .version = DRM_EVENT_CONTEXT_VERSION,
+      .page_flip_handler = page_flip_complete
+    };
+
+    ret = drmHandleEvent(drm_fd, &evctx);
+    if (ret != 0) {
+      printf("drmHandleEvent failed ret=%d\n", ret);
+      break;
+    }
+  }
+
   current_buffer = 1 - current_buffer;
   return GRSurfaceDrms[current_buffer];
 }
diff --git a/minui/graphics_drm.h b/minui/graphics_drm.h
index de96212..756625b 100644
--- a/minui/graphics_drm.h
+++ b/minui/graphics_drm.h
@@ -42,7 +42,7 @@
 
  private:
   void DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc);
-  void DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface);
+  int DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface);
   GRSurfaceDrm* DrmCreateSurface(int width, int height);
   void DrmDestroySurface(GRSurfaceDrm* surface);
   void DisableNonMainCrtcs(int fd, drmModeRes* resources, drmModeCrtc* main_crtc);
diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h
index f9da199..fa13ecd 100644
--- a/minui/include/minui/minui.h
+++ b/minui/include/minui/minui.h
@@ -41,14 +41,27 @@
   int char_height;
 };
 
-enum GRRotation {
-  ROTATION_NONE = 0,
-  ROTATION_RIGHT = 1,
-  ROTATION_DOWN = 2,
-  ROTATION_LEFT = 3,
+enum class GRRotation : int {
+  NONE = 0,
+  RIGHT = 1,
+  DOWN = 2,
+  LEFT = 3,
 };
 
+enum class PixelFormat : int {
+  UNKNOWN = 0,
+  ABGR = 1,
+  RGBX = 2,
+  BGRA = 3,
+};
+
+// Initializes the graphics backend and loads font file. Returns 0 on success, or -1 on error. Note
+// that the font initialization failure would be non-fatal, as caller may not need to draw any text
+// at all. Caller can check the font initialization result via gr_sys_font() as needed.
 int gr_init();
+
+// Frees the allocated resources. The function is idempotent, and safe to be called if gr_init()
+// didn't finish successfully.
 void gr_exit();
 
 int gr_fb_width();
@@ -57,7 +70,8 @@
 void gr_flip();
 void gr_fb_blank(bool blank);
 
-void gr_clear();  // clear entire surface to current color
+// Clears entire surface to current color.
+void gr_clear();
 void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
 void gr_fill(int x1, int y1, int x2, int y2);
 
@@ -66,16 +80,21 @@
 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);
+// Returns -1 if font is nullptr.
 int gr_measure(const GRFont* font, const char* s);
-void gr_font_size(const GRFont* font, int* x, int* y);
+// Returns -1 if font is nullptr.
+int 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);
+unsigned int gr_get_width(const GRSurface* surface);
+unsigned int gr_get_height(const GRSurface* surface);
 
-// Set rotation, flips gr_fb_width/height if 90 degree rotation difference
+// Sets rotation, flips gr_fb_width/height if 90 degree rotation difference
 void gr_rotate(GRRotation rotation);
 
+// Returns the current PixelFormat being used.
+PixelFormat gr_pixel_format();
+
 //
 // Input events.
 //
diff --git a/minui/include/private/resources.h b/minui/include/private/resources.h
new file mode 100644
index 0000000..047ebe2
--- /dev/null
+++ b/minui/include/private/resources.h
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+#include <memory>
+#include <string>
+
+#include <png.h>
+
+// 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:
+  // Constructs an instance by loading the PNG file from '/res/images/<name>.png', or '<name>'.
+  PngHandler(const std::string& name);
+
+  ~PngHandler();
+
+  png_uint_32 width() const {
+    return width_;
+  }
+
+  png_uint_32 height() const {
+    return height_;
+  }
+
+  png_byte channels() const {
+    return channels_;
+  }
+
+  int bit_depth() const {
+    return bit_depth_;
+  }
+
+  int color_type() const {
+    return color_type_;
+  }
+
+  png_structp png_ptr() const {
+    return png_ptr_;
+  }
+
+  png_infop info_ptr() const {
+    return info_ptr_;
+  }
+
+  int error_code() const {
+    return error_code_;
+  };
+
+  operator bool() const {
+    return error_code_ == 0;
+  }
+
+ private:
+  png_structp png_ptr_{ nullptr };
+  png_infop info_ptr_{ nullptr };
+  png_uint_32 width_;
+  png_uint_32 height_;
+  png_byte channels_;
+  int bit_depth_;
+  int color_type_;
+
+  // The |error_code_| is set to a negative value if an error occurs when opening the png file.
+  int error_code_{ 0 };
+  // After initialization, we'll keep the file pointer open before destruction of PngHandler.
+  std::unique_ptr<FILE, decltype(&fclose)> png_fp_{ nullptr, fclose };
+};
+
+// Overrides the default resource dir, for testing purpose.
+void res_set_resource_dir(const std::string&);
diff --git a/minui/mkfont.c b/minui/mkfont.c
deleted file mode 100644
index 61a5ede..0000000
--- a/minui/mkfont.c
+++ /dev/null
@@ -1,54 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-int main(int argc, char *argv)
-{
-    unsigned n;
-    unsigned char *x;
-    unsigned m;
-    unsigned run_val;
-    unsigned run_count;
- 
-    n = gimp_image.width * gimp_image.height;
-    m = 0;
-    x = gimp_image.pixel_data;
-
-    printf("struct {\n");
-    printf("  unsigned width;\n");
-    printf("  unsigned height;\n");
-    printf("  unsigned cwidth;\n");
-    printf("  unsigned cheight;\n");
-    printf("  unsigned char rundata[];\n");
-    printf("} font = {\n");
-    printf("  .width = %d,\n  .height = %d,\n  .cwidth = %d,\n  .cheight = %d,\n", gimp_image.width, gimp_image.height,
-           gimp_image.width / 96, gimp_image.height);
-    printf("  .rundata = {\n");
-   
-    run_val = (*x ? 0 : 255);
-    run_count = 1;
-    n--;
-    x+=3;
-
-    while(n-- > 0) {
-        unsigned val = (*x ? 0 : 255);
-        x+=3;
-        if((val == run_val) && (run_count < 127)) {
-            run_count++;
-        } else {
-eject:
-            printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
-            run_val = val;
-            run_count = 1;
-            m += 5;
-            if(m >= 75) {
-                printf("\n");
-                m = 0;
-            }
-        }
-    }
-    printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
-    printf("\n0x00,");
-    printf("\n");
-    printf("  }\n};\n");
-    return 0;
-}
diff --git a/minui/resources.cpp b/minui/resources.cpp
index 52ab60b..477fbe2 100644
--- a/minui/resources.cpp
+++ b/minui/resources.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include "private/resources.h"
+
 #include <fcntl.h>
 #include <linux/fb.h>
 #include <linux/kd.h>
@@ -30,7 +32,6 @@
 #include <string>
 #include <vector>
 
-#include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <png.h>
 
@@ -38,6 +39,8 @@
 
 #define SURFACE_DATA_ALIGNMENT 8
 
+static std::string g_resource_dir{ "/res/images" };
+
 static GRSurface* malloc_surface(size_t data_size) {
     size_t size = sizeof(GRSurface) + data_size + SURFACE_DATA_ALIGNMENT;
     unsigned char* temp = static_cast<unsigned char*>(malloc(size));
@@ -48,58 +51,13 @@
     return surface;
 }
 
-// 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);
-
-  ~PngHandler();
-
-  png_uint_32 width() const {
-    return width_;
-  }
-
-  png_uint_32 height() const {
-    return height_;
-  }
-
-  png_byte channels() const {
-    return channels_;
-  }
-
-  png_structp png_ptr() const {
-    return png_ptr_;
-  }
-
-  png_infop info_ptr() const {
-    return info_ptr_;
-  }
-
-  int error_code() const {
-    return error_code_;
-  };
-
-  operator bool() const {
-    return error_code_ == 0;
-  }
-
- private:
-  png_structp png_ptr_{ nullptr };
-  png_infop info_ptr_{ nullptr };
-  png_uint_32 width_;
-  png_uint_32 height_;
-  png_byte channels_;
-
-  // 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_;
-};
-
-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());
+PngHandler::PngHandler(const std::string& name) {
+  std::string res_path = g_resource_dir + "/" + name + ".png";
   png_fp_.reset(fopen(res_path.c_str(), "rbe"));
+  // Try to read from |name| if the resource path does not work.
+  if (!png_fp_) {
+    png_fp_.reset(fopen(name.c_str(), "rbe"));
+  }
   if (!png_fp_) {
     error_code_ = -1;
     return;
@@ -138,19 +96,17 @@
   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,
+  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) {
+  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) {
+  } 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) {
+  } 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
@@ -158,8 +114,8 @@
     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);
+    fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n", bit_depth_,
+            channels_, color_type_);
     error_code_ = -7;
   }
 }
@@ -251,9 +207,10 @@
     return -8;
   }
 
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-  png_set_bgr(png_ptr);
-#endif
+  PixelFormat pixel_format = gr_pixel_format();
+  if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+    png_set_bgr(png_ptr);
+  }
 
   for (png_uint_32 y = 0; y < height; ++y) {
     std::vector<unsigned char> p_row(width * 4);
@@ -322,9 +279,9 @@
     }
   }
 
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-  png_set_bgr(png_ptr);
-#endif
+  if (gr_pixel_format() == PixelFormat::ABGR || gr_pixel_format() == PixelFormat::BGRA) {
+    png_set_bgr(png_ptr);
+  }
 
   for (png_uint_32 y = 0; y < height; ++y) {
     std::vector<unsigned char> p_row(width * 4);
@@ -371,9 +328,10 @@
   surface->row_bytes = width;
   surface->pixel_bytes = 1;
 
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-  png_set_bgr(png_ptr);
-#endif
+  PixelFormat pixel_format = gr_pixel_format();
+  if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+    png_set_bgr(png_ptr);
+  }
 
   for (png_uint_32 y = 0; y < height; ++y) {
     unsigned char* p_row = surface->data + y * surface->row_bytes;
@@ -385,6 +343,10 @@
   return 0;
 }
 
+void res_set_resource_dir(const std::string& dirname) {
+  g_resource_dir = dirname;
+}
+
 // This function tests if a locale string stored in PNG (prefix) matches
 // the locale string provided by the system (locale).
 bool matches_locale(const std::string& prefix, const std::string& locale) {
diff --git a/otafault/Android.bp b/otafault/Android.bp
deleted file mode 100644
index b39d5be..0000000
--- a/otafault/Android.bp
+++ /dev/null
@@ -1,68 +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.
-
-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/config.cpp b/otafault/config.cpp
deleted file mode 100644
index 3993948..0000000
--- a/otafault/config.cpp
+++ /dev/null
@@ -1,75 +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.
- */
-
-#include "otafault/config.h"
-
-#include <map>
-#include <string>
-
-#include <android-base/stringprintf.h>
-#include <ziparchive/zip_archive.h>
-
-#include "otafault/ota_io.h"
-
-#define OTAIO_MAX_FNAME_SIZE 128
-
-static ZipArchiveHandle archive;
-static bool is_retry = false;
-static std::map<std::string, bool> should_inject_cache;
-
-static std::string get_type_path(const char* io_type) {
-    return android::base::StringPrintf("%s/%s", OTAIO_BASE_DIR, io_type);
-}
-
-void ota_io_init(ZipArchiveHandle za, bool retry) {
-    archive = za;
-    is_retry = retry;
-    ota_set_fault_files();
-}
-
-bool should_fault_inject(const char* io_type) {
-    // archive will be NULL if we used an entry point other
-    // than updater/updater.cpp:main
-    if (archive == nullptr || is_retry) {
-        return false;
-    }
-    const std::string type_path = get_type_path(io_type);
-    if (should_inject_cache.find(type_path) != should_inject_cache.end()) {
-        return should_inject_cache[type_path];
-    }
-    ZipString zip_type_path(type_path.c_str());
-    ZipEntry entry;
-    int status = FindEntry(archive, zip_type_path, &entry);
-    should_inject_cache[type_path] = (status == 0);
-    return (status == 0);
-}
-
-bool should_hit_cache() {
-    return should_fault_inject(OTAIO_CACHE);
-}
-
-std::string fault_fname(const char* io_type) {
-    std::string type_path = get_type_path(io_type);
-    std::string fname;
-    fname.resize(OTAIO_MAX_FNAME_SIZE);
-    ZipString zip_type_path(type_path.c_str());
-    ZipEntry 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/include/otafault/config.h b/otafault/include/otafault/config.h
deleted file mode 100644
index cc4bfd2..0000000
--- a/otafault/include/otafault/config.h
+++ /dev/null
@@ -1,72 +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.
- */
-
-/*
- * Read configuration files in the OTA package to determine which files, if any, will trigger
- * errors.
- *
- * OTA packages can be modified to trigger errors by adding a top-level directory called
- * .libotafault, which may optionally contain up to three files called READ, WRITE, and FSYNC.
- * Each one of these optional files contains the name of a single file on the device disk which
- * will cause an IO error on the first call of the appropriate I/O action to that file.
- *
- * Example:
- * ota.zip
- *   <normal package contents>
- *   .libotafault
- *     WRITE
- *
- * If the contents of the file WRITE were /system/build.prop, the first write action to
- * /system/build.prop would fail with EIO. Note that READ and FSYNC files are absent, so these
- * actions will not cause an error.
- */
-
-#ifndef _UPDATER_OTA_IO_CFG_H_
-#define _UPDATER_OTA_IO_CFG_H_
-
-#include <string>
-
-#include <ziparchive/zip_archive.h>
-
-#define OTAIO_BASE_DIR ".libotafault"
-#define OTAIO_READ "READ"
-#define OTAIO_WRITE "WRITE"
-#define OTAIO_FSYNC "FSYNC"
-#define OTAIO_CACHE "CACHE"
-
-/*
- * Initialize libotafault by providing a reference to the OTA package.
- */
-void ota_io_init(ZipArchiveHandle zip, bool retry);
-
-/*
- * Return true if a config file is present for the given IO type.
- */
-bool should_fault_inject(const char* io_type);
-
-/*
- * Return true if an EIO should occur on the next hit to /cache/saved.file
- * instead of the next hit to the specified file.
- */
-bool should_hit_cache();
-
-/*
- * Return the name of the file that should cause an error for the
- * given IO type.
- */
-std::string fault_fname(const char* io_type);
-
-#endif
diff --git a/otafault/include/otafault/ota_io.h b/otafault/include/otafault/ota_io.h
deleted file mode 100644
index 45e481a..0000000
--- a/otafault/include/otafault/ota_io.h
+++ /dev/null
@@ -1,70 +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.
- */
-
-/*
- * Provide a series of proxy functions for basic file accessors.
- * The behavior of these functions can be changed to return different
- * errors under a variety of conditions.
- */
-
-#ifndef _UPDATER_OTA_IO_H_
-#define _UPDATER_OTA_IO_H_
-
-#include <stddef.h>
-#include <stdio.h>
-#include <sys/stat.h>  // mode_t
-
-#include <memory>
-
-#include <android-base/unique_fd.h>
-
-#define OTAIO_CACHE_FNAME "/cache/saved.file"
-
-void ota_set_fault_files();
-
-int ota_open(const char* path, int oflags);
-
-int ota_open(const char* path, int oflags, mode_t mode);
-
-FILE* ota_fopen(const char* filename, const char* mode);
-
-size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream);
-
-ssize_t ota_read(int fd, void* buf, size_t nbyte);
-
-size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
-
-ssize_t ota_write(int fd, const void* buf, size_t nbyte);
-
-int ota_fsync(int fd);
-
-struct OtaCloser {
-  static void Close(int);
-};
-
-using unique_fd = android::base::unique_fd_impl<OtaCloser>;
-
-int ota_close(unique_fd& fd);
-
-struct OtaFcloser {
-  void operator()(FILE*) const;
-};
-
-using unique_file = std::unique_ptr<FILE, OtaFcloser>;
-
-int ota_fclose(unique_file& fh);
-
-#endif
diff --git a/otafault/ota_io.cpp b/otafault/ota_io.cpp
deleted file mode 100644
index 63ef18e..0000000
--- a/otafault/ota_io.cpp
+++ /dev/null
@@ -1,212 +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.
- */
-
-#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 <mutex>
-#include <string>
-
-#include <android-base/thread_annotations.h>
-
-#include "otafault/config.h"
-
-static std::mutex filename_mutex;
-static std::map<intptr_t, const char*> filename_cache GUARDED_BY(filename_mutex);
-static std::string read_fault_file_name = "";
-static std::string write_fault_file_name = "";
-static std::string fsync_fault_file_name = "";
-
-static bool get_hit_file(const char* cached_path, const std::string& ffn) {
-    return should_hit_cache()
-        ? !strncmp(cached_path, OTAIO_CACHE_FNAME, strlen(cached_path))
-        : !strncmp(cached_path, ffn.c_str(), strlen(cached_path));
-}
-
-void ota_set_fault_files() {
-    if (should_fault_inject(OTAIO_READ)) {
-        read_fault_file_name = fault_fname(OTAIO_READ);
-    }
-    if (should_fault_inject(OTAIO_WRITE)) {
-        write_fault_file_name = fault_fname(OTAIO_WRITE);
-    }
-    if (should_fault_inject(OTAIO_FSYNC)) {
-        fsync_fault_file_name = fault_fname(OTAIO_FSYNC);
-    }
-}
-
-bool have_eio_error = false;
-
-int ota_open(const char* path, int oflags) {
-    // Let the caller handle errors; we do not care if open succeeds or fails
-    int fd = open(path, oflags);
-    std::lock_guard<std::mutex> lock(filename_mutex);
-    filename_cache[fd] = path;
-    return fd;
-}
-
-int ota_open(const char* path, int oflags, mode_t mode) {
-    int fd = open(path, oflags, mode);
-    std::lock_guard<std::mutex> lock(filename_mutex);
-    filename_cache[fd] = path;
-    return fd;
-}
-
-FILE* ota_fopen(const char* path, const char* mode) {
-    FILE* fh = fopen(path, mode);
-    std::lock_guard<std::mutex> lock(filename_mutex);
-    filename_cache[(intptr_t)fh] = path;
-    return fh;
-}
-
-static int __ota_close(int fd) {
-    // descriptors can be reused, so make sure not to leave them in the cache
-    std::lock_guard<std::mutex> lock(filename_mutex);
-    filename_cache.erase(fd);
-    return close(fd);
-}
-
-void OtaCloser::Close(int fd) {
-    __ota_close(fd);
-}
-
-int ota_close(unique_fd& fd) {
-    return __ota_close(fd.release());
-}
-
-static int __ota_fclose(FILE* fh) {
-    std::lock_guard<std::mutex> lock(filename_mutex);
-    filename_cache.erase(reinterpret_cast<intptr_t>(fh));
-    return fclose(fh);
-}
-
-void OtaFcloser::operator()(FILE* f) const {
-    __ota_fclose(f);
-};
-
-int ota_fclose(unique_file& fh) {
-  return __ota_fclose(fh.release());
-}
-
-size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream) {
-    if (should_fault_inject(OTAIO_READ)) {
-        std::lock_guard<std::mutex> lock(filename_mutex);
-        auto cached = filename_cache.find((intptr_t)stream);
-        const char* cached_path = cached->second;
-        if (cached != filename_cache.end() &&
-                get_hit_file(cached_path, read_fault_file_name)) {
-            read_fault_file_name = "";
-            errno = EIO;
-            have_eio_error = true;
-            return 0;
-        }
-    }
-    size_t status = fread(ptr, size, nitems, stream);
-    // If I/O error occurs, set the retry-update flag.
-    if (status != nitems && errno == EIO) {
-        have_eio_error = true;
-    }
-    return status;
-}
-
-ssize_t ota_read(int fd, void* buf, size_t nbyte) {
-    if (should_fault_inject(OTAIO_READ)) {
-        std::lock_guard<std::mutex> lock(filename_mutex);
-        auto cached = filename_cache.find(fd);
-        const char* cached_path = cached->second;
-        if (cached != filename_cache.end()
-                && get_hit_file(cached_path, read_fault_file_name)) {
-            read_fault_file_name = "";
-            errno = EIO;
-            have_eio_error = true;
-            return -1;
-        }
-    }
-    ssize_t status = read(fd, buf, nbyte);
-    if (status == -1 && errno == EIO) {
-        have_eio_error = true;
-    }
-    return status;
-}
-
-size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream) {
-    if (should_fault_inject(OTAIO_WRITE)) {
-        std::lock_guard<std::mutex> lock(filename_mutex);
-        auto cached = filename_cache.find((intptr_t)stream);
-        const char* cached_path = cached->second;
-        if (cached != filename_cache.end() &&
-                get_hit_file(cached_path, write_fault_file_name)) {
-            write_fault_file_name = "";
-            errno = EIO;
-            have_eio_error = true;
-            return 0;
-        }
-    }
-    size_t status = fwrite(ptr, size, count, stream);
-    if (status != count && errno == EIO) {
-        have_eio_error = true;
-    }
-    return status;
-}
-
-ssize_t ota_write(int fd, const void* buf, size_t nbyte) {
-    if (should_fault_inject(OTAIO_WRITE)) {
-        std::lock_guard<std::mutex> lock(filename_mutex);
-        auto cached = filename_cache.find(fd);
-        const char* cached_path = cached->second;
-        if (cached != filename_cache.end() &&
-                get_hit_file(cached_path, write_fault_file_name)) {
-            write_fault_file_name = "";
-            errno = EIO;
-            have_eio_error = true;
-            return -1;
-        }
-    }
-    ssize_t status = write(fd, buf, nbyte);
-    if (status == -1 && errno == EIO) {
-        have_eio_error = true;
-    }
-    return status;
-}
-
-int ota_fsync(int fd) {
-    if (should_fault_inject(OTAIO_FSYNC)) {
-        std::lock_guard<std::mutex> lock(filename_mutex);
-        auto cached = filename_cache.find(fd);
-        const char* cached_path = cached->second;
-        if (cached != filename_cache.end() &&
-                get_hit_file(cached_path, fsync_fault_file_name)) {
-            fsync_fault_file_name = "";
-            errno = EIO;
-            have_eio_error = true;
-            return -1;
-        }
-    }
-    int status = fsync(fd);
-    if (status == -1 && errno == EIO) {
-        have_eio_error = true;
-    }
-    return status;
-}
-
diff --git a/otafault/test.cpp b/otafault/test.cpp
deleted file mode 100644
index 63e2445..0000000
--- a/otafault/test.cpp
+++ /dev/null
@@ -1,35 +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.
- */
-
-#include <fcntl.h>
-#include <stdio.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include "otafault/ota_io.h"
-
-int main(int /* argc */, char** /* argv */) {
-    int fd = open("testdata/test.file", O_RDWR);
-    char buf[8];
-    const char* out = "321";
-    int readv = ota_read(fd, buf, 4);
-    printf("Read returned %d\n", readv);
-    int writev = ota_write(fd, out, 4);
-    printf("Write returned %d\n", writev);
-    close(fd);
-    return 0;
-}
diff --git a/otautil/Android.bp b/otautil/Android.bp
index 75cf691..41018dd 100644
--- a/otautil/Android.bp
+++ b/otautil/Android.bp
@@ -16,27 +16,40 @@
     name: "libotautil",
 
     host_supported: true,
+    recovery_available: true,
 
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    // Minimal set of files to support host build.
     srcs: [
-        "SysUtil.cpp",
-        "DirUtil.cpp",
-        "ThermalUtil.cpp",
-        "cache_location.cpp",
+        "paths.cpp",
         "rangeset.cpp",
     ],
 
-    static_libs: [
-        "libselinux",
+    shared_libs: [
         "libbase",
     ],
 
-    cflags: [
-        "-D_FILE_OFFSET_BITS=64",
-        "-Werror",
-        "-Wall",
-    ],
-
     export_include_dirs: [
         "include",
     ],
+
+    target: {
+        android: {
+            srcs: [
+                "dirutil.cpp",
+                "mounts.cpp",
+                "parse_install_logs.cpp",
+                "sysutil.cpp",
+                "thermalutil.cpp",
+            ],
+
+            shared_libs: [
+                "libcutils",
+                "libselinux",
+            ],
+        },
+    },
 }
diff --git a/otautil/cache_location.cpp b/otautil/cache_location.cpp
deleted file mode 100644
index 8ddefec..0000000
--- a/otautil/cache_location.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * 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/DirUtil.cpp b/otautil/dirutil.cpp
similarity index 98%
rename from otautil/DirUtil.cpp
rename to otautil/dirutil.cpp
index 61c8328..ae1cd5c 100644
--- a/otautil/DirUtil.cpp
+++ b/otautil/dirutil.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "otautil/DirUtil.h"
+#include "otautil/dirutil.h"
 
 #include <dirent.h>
 #include <errno.h>
diff --git a/otautil/include/otautil/cache_location.h b/otautil/include/otautil/cache_location.h
deleted file mode 100644
index f2f6638..0000000
--- a/otautil/include/otautil/cache_location.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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/otautil/include/otautil/DirUtil.h b/otautil/include/otautil/dirutil.h
similarity index 100%
rename from otautil/include/otautil/DirUtil.h
rename to otautil/include/otautil/dirutil.h
diff --git a/otautil/include/otautil/error_code.h b/otautil/include/otautil/error_code.h
index b0ff42d..2b73c13 100644
--- a/otautil/include/otautil/error_code.h
+++ b/otautil/include/otautil/error_code.h
@@ -48,6 +48,8 @@
   kRebootFailure,
   kPackageExtractFileFailure,
   kPatchApplicationFailure,
+  kHashTreeComputationFailure,
+  kEioFailure,
   kVendorFailure = 200
 };
 
diff --git a/mounts.h b/otautil/include/otautil/mounts.h
similarity index 94%
rename from mounts.h
rename to otautil/include/otautil/mounts.h
index 0de1ebd..6786c8d 100644
--- a/mounts.h
+++ b/otautil/include/otautil/mounts.h
@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
 
 struct MountedVolume;
 
@@ -24,5 +23,3 @@
 MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
 
 int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
diff --git a/otautil/include/otautil/parse_install_logs.h b/otautil/include/otautil/parse_install_logs.h
new file mode 100644
index 0000000..135d29c
--- /dev/null
+++ b/otautil/include/otautil/parse_install_logs.h
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+constexpr const char* LAST_INSTALL_FILE = "/data/misc/recovery/last_install";
+constexpr const char* LAST_INSTALL_FILE_IN_CACHE = "/cache/recovery/last_install";
+
+// Parses the metrics of update applied under recovery mode in |lines|, and returns a map with
+// "name: value".
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines);
+// Parses the sideload history and update metrics in the last_install file. Returns a map with
+// entries as "metrics_name: value". If no such file exists, returns an empty map.
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name);
diff --git a/otautil/include/otautil/paths.h b/otautil/include/otautil/paths.h
new file mode 100644
index 0000000..f95741a
--- /dev/null
+++ b/otautil/include/otautil/paths.h
@@ -0,0 +1,118 @@
+/*
+ * 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_PATHS_H_
+#define _OTAUTIL_PATHS_H_
+
+#include <string>
+
+#include <android-base/macros.h>
+
+// A singleton class to maintain the update related paths. The paths should be only set once at the
+// start of the program.
+class Paths {
+ public:
+  static Paths& Get();
+
+  std::string cache_log_directory() const {
+    return cache_log_directory_;
+  }
+  void set_cache_log_directory(const std::string& log_dir) {
+    cache_log_directory_ = log_dir;
+  }
+
+  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_file) {
+    last_command_file_ = last_command_file;
+  }
+
+  std::string resource_dir() const {
+    return resource_dir_;
+  }
+  void set_resource_dir(const std::string& resource_dir) {
+    resource_dir_ = resource_dir;
+  }
+
+  std::string stash_directory_base() const {
+    return stash_directory_base_;
+  }
+  void set_stash_directory_base(const std::string& base) {
+    stash_directory_base_ = base;
+  }
+
+  std::string temporary_install_file() const {
+    return temporary_install_file_;
+  }
+  void set_temporary_install_file(const std::string& install_file) {
+    temporary_install_file_ = install_file;
+  }
+
+  std::string temporary_log_file() const {
+    return temporary_log_file_;
+  }
+  void set_temporary_log_file(const std::string& log_file) {
+    temporary_log_file_ = log_file;
+  }
+
+  std::string temporary_update_binary() const {
+    return temporary_update_binary_;
+  }
+  void set_temporary_update_binary(const std::string& update_binary) {
+    temporary_update_binary_ = update_binary;
+  }
+
+ private:
+  Paths();
+  DISALLOW_COPY_AND_ASSIGN(Paths);
+
+  // Path to the directory that contains last_log and last_kmsg log files.
+  std::string cache_log_directory_;
+
+  // Path to the temporary source file on /cache. 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.
+  std::string cache_temp_source_;
+
+  // Path to the last command file.
+  std::string last_command_file_;
+
+  // Path to the resource dir;
+  std::string resource_dir_;
+
+  // Path to the base directory to write stashes during update.
+  std::string stash_directory_base_;
+
+  // Path to the temporary file that contains the install result.
+  std::string temporary_install_file_;
+
+  // Path to the temporary log file while under recovery.
+  std::string temporary_log_file_;
+
+  // Path to the temporary update binary while installing a non-A/B package.
+  std::string temporary_update_binary_;
+};
+
+#endif  // _OTAUTIL_PATHS_H_
diff --git a/otautil/include/otautil/SysUtil.h b/otautil/include/otautil/sysutil.h
similarity index 71%
rename from otautil/include/otautil/SysUtil.h
rename to otautil/include/otautil/sysutil.h
index 52f6d20..2eeb7c3 100644
--- a/otautil/include/otautil/SysUtil.h
+++ b/otautil/include/otautil/sysutil.h
@@ -50,4 +50,13 @@
   std::vector<MappedRange> ranges_;
 };
 
+// Wrapper function to trigger a reboot, by additionally handling quiescent reboot mode. The
+// command should start with "reboot," (e.g. "reboot,bootloader" or "reboot,").
+bool reboot(const std::string& command);
+
+// Returns a null-terminated char* array, where the elements point to the C-strings in the given
+// vector, plus an additional nullptr at the end. This is a helper function that facilitates
+// calling C functions (such as getopt(3)) that expect an array of C-strings.
+std::vector<char*> StringVectorToNullTerminatedArray(const std::vector<std::string>& args);
+
 #endif  // _OTAUTIL_SYSUTIL
diff --git a/otautil/include/otautil/ThermalUtil.h b/otautil/include/otautil/thermalutil.h
similarity index 100%
rename from otautil/include/otautil/ThermalUtil.h
rename to otautil/include/otautil/thermalutil.h
diff --git a/mounts.cpp b/otautil/mounts.cpp
similarity index 60%
rename from mounts.cpp
rename to otautil/mounts.cpp
index 76fa657..951311b 100644
--- a/mounts.cpp
+++ b/otautil/mounts.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "mounts.h"
+#include "otautil/mounts.h"
 
 #include <errno.h>
 #include <fcntl.h>
@@ -30,43 +30,43 @@
 #include <android-base/logging.h>
 
 struct MountedVolume {
-    std::string device;
-    std::string mount_point;
-    std::string filesystem;
-    std::string flags;
+  std::string device;
+  std::string mount_point;
+  std::string filesystem;
+  std::string flags;
 };
 
-std::vector<MountedVolume*> g_mounts_state;
+static std::vector<MountedVolume*> g_mounts_state;
 
 bool scan_mounted_volumes() {
-    for (size_t i = 0; i < g_mounts_state.size(); ++i) {
-        delete g_mounts_state[i];
-    }
-    g_mounts_state.clear();
+  for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+    delete g_mounts_state[i];
+  }
+  g_mounts_state.clear();
 
-    // Open and read mount table entries.
-    FILE* fp = setmntent("/proc/mounts", "re");
-    if (fp == NULL) {
-        return false;
-    }
-    mntent* e;
-    while ((e = getmntent(fp)) != NULL) {
-        MountedVolume* v = new MountedVolume;
-        v->device = e->mnt_fsname;
-        v->mount_point = e->mnt_dir;
-        v->filesystem = e->mnt_type;
-        v->flags = e->mnt_opts;
-        g_mounts_state.push_back(v);
-    }
-    endmntent(fp);
-    return true;
+  // Open and read mount table entries.
+  FILE* fp = setmntent("/proc/mounts", "re");
+  if (fp == NULL) {
+    return false;
+  }
+  mntent* e;
+  while ((e = getmntent(fp)) != NULL) {
+    MountedVolume* v = new MountedVolume;
+    v->device = e->mnt_fsname;
+    v->mount_point = e->mnt_dir;
+    v->filesystem = e->mnt_type;
+    v->flags = e->mnt_opts;
+    g_mounts_state.push_back(v);
+  }
+  endmntent(fp);
+  return true;
 }
 
 MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point) {
-    for (size_t i = 0; i < g_mounts_state.size(); ++i) {
-        if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i];
-    }
-    return nullptr;
+  for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+    if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i];
+  }
+  return nullptr;
 }
 
 int unmount_mounted_volume(MountedVolume* volume) {
diff --git a/otautil/parse_install_logs.cpp b/otautil/parse_install_logs.cpp
new file mode 100644
index 0000000..13a7299
--- /dev/null
+++ b/otautil/parse_install_logs.cpp
@@ -0,0 +1,114 @@
+/*
+ * 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/parse_install_logs.h"
+
+#include <unistd.h>
+
+#include <optional>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+
+constexpr const char* OTA_SIDELOAD_METRICS = "ota_sideload";
+
+// Here is an example of lines in last_install:
+// ...
+// time_total: 101
+// bytes_written_vendor: 51074
+// bytes_stashed_vendor: 200
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines) {
+  constexpr unsigned int kMiB = 1024 * 1024;
+  std::optional<int64_t> bytes_written_in_mib;
+  std::optional<int64_t> bytes_stashed_in_mib;
+  std::map<std::string, int64_t> metrics;
+  for (const auto& line : lines) {
+    size_t num_index = line.find(':');
+    if (num_index == std::string::npos) {
+      LOG(WARNING) << "Skip parsing " << line;
+      continue;
+    }
+
+    std::string num_string = android::base::Trim(line.substr(num_index + 1));
+    int64_t parsed_num;
+    if (!android::base::ParseInt(num_string, &parsed_num)) {
+      LOG(ERROR) << "Failed to parse numbers in " << line;
+      continue;
+    }
+
+    if (android::base::StartsWith(line, "bytes_written")) {
+      bytes_written_in_mib = bytes_written_in_mib.value_or(0) + parsed_num / kMiB;
+    } else if (android::base::StartsWith(line, "bytes_stashed")) {
+      bytes_stashed_in_mib = bytes_stashed_in_mib.value_or(0) + parsed_num / kMiB;
+    } else if (android::base::StartsWith(line, "time")) {
+      metrics.emplace("ota_time_total", parsed_num);
+    } else if (android::base::StartsWith(line, "uncrypt_time")) {
+      metrics.emplace("ota_uncrypt_time", parsed_num);
+    } else if (android::base::StartsWith(line, "source_build")) {
+      metrics.emplace("ota_source_version", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_start")) {
+      metrics.emplace("ota_temperature_start", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_end")) {
+      metrics.emplace("ota_temperature_end", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_max")) {
+      metrics.emplace("ota_temperature_max", parsed_num);
+    } else if (android::base::StartsWith(line, "error")) {
+      metrics.emplace("ota_non_ab_error_code", parsed_num);
+    } else if (android::base::StartsWith(line, "cause")) {
+      metrics.emplace("ota_non_ab_cause_code", parsed_num);
+    }
+  }
+
+  if (bytes_written_in_mib) {
+    metrics.emplace("ota_written_in_MiBs", bytes_written_in_mib.value());
+  }
+  if (bytes_stashed_in_mib) {
+    metrics.emplace("ota_stashed_in_MiBs", bytes_stashed_in_mib.value());
+  }
+
+  return metrics;
+}
+
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name) {
+  if (access(file_name.c_str(), F_OK) != 0) {
+    return {};
+  }
+
+  std::string content;
+  if (!android::base::ReadFileToString(file_name, &content)) {
+    PLOG(ERROR) << "Failed to read " << file_name;
+    return {};
+  }
+
+  if (content.empty()) {
+    LOG(INFO) << "Empty last_install file";
+    return {};
+  }
+
+  std::vector<std::string> lines = android::base::Split(content, "\n");
+  auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+  // LAST_INSTALL starts with "/sideload/package.zip" after a sideload.
+  if (android::base::Trim(lines[0]) == "/sideload/package.zip") {
+    int type = (android::base::GetProperty("ro.build.type", "") == "user") ? 1 : 0;
+    metrics.emplace(OTA_SIDELOAD_METRICS, type);
+  }
+
+  return metrics;
+}
diff --git a/otautil/paths.cpp b/otautil/paths.cpp
new file mode 100644
index 0000000..33ab4a5
--- /dev/null
+++ b/otautil/paths.cpp
@@ -0,0 +1,41 @@
+/*
+ * 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/paths.h"
+
+constexpr const char kDefaultCacheLogDirectory[] = "/cache/recovery";
+constexpr const char kDefaultCacheTempSource[] = "/cache/saved.file";
+constexpr const char kDefaultLastCommandFile[] = "/cache/recovery/last_command";
+constexpr const char kDefaultResourceDirectory[] = "/res/images";
+constexpr const char kDefaultStashDirectoryBase[] = "/cache/recovery";
+constexpr const char kDefaultTemporaryInstallFile[] = "/tmp/last_install";
+constexpr const char kDefaultTemporaryLogFile[] = "/tmp/recovery.log";
+constexpr const char kDefaultTemporaryUpdateBinary[] = "/tmp/update-binary";
+
+Paths& Paths::Get() {
+  static Paths paths;
+  return paths;
+}
+
+Paths::Paths()
+    : cache_log_directory_(kDefaultCacheLogDirectory),
+      cache_temp_source_(kDefaultCacheTempSource),
+      last_command_file_(kDefaultLastCommandFile),
+      resource_dir_(kDefaultResourceDirectory),
+      stash_directory_base_(kDefaultStashDirectoryBase),
+      temporary_install_file_(kDefaultTemporaryInstallFile),
+      temporary_log_file_(kDefaultTemporaryLogFile),
+      temporary_update_binary_(kDefaultTemporaryUpdateBinary) {}
diff --git a/otautil/rangeset.cpp b/otautil/rangeset.cpp
index 96955b9..5ab8e08 100644
--- a/otautil/rangeset.cpp
+++ b/otautil/rangeset.cpp
@@ -148,8 +148,8 @@
     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);
+  for (const auto& [begin, end] : ranges_) {
+    result += android::base::StringPrintf(",%zu,%zu", begin, end);
   }
 
   return result;
@@ -159,11 +159,11 @@
 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;
+  for (const auto& [begin, end] : ranges_) {
+    if (idx < end - begin) {
+      return begin + idx;
     }
-    idx -= (range.second - range.first);
+    idx -= (end - begin);
   }
 
   CHECK(false) << "Failed to find block number for index " << idx;
@@ -173,14 +173,10 @@
 // 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)) {
+  for (const auto& [begin, end] : ranges_) {
+    for (const auto& [other_begin, other_end] : other.ranges_) {
+      // [begin, end) vs [other_begin, other_end)
+      if (!(other_begin >= end || begin >= other_end)) {
         return true;
       }
     }
@@ -248,20 +244,20 @@
 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_) {
+  for (const auto& [start, end] : 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);
+    if (old_block_start >= end) {
+      new_block_start += (end - start);
+    } else if (old_block_start >= start) {
+      new_block_start += (old_block_start - start);
       return (new_block_start * kBlockSize + old_offset % kBlockSize);
     } else {
       CHECK(false) << "block_start " << old_block_start
-                   << " is missing between two ranges: " << this->ToString();
+                   << " is missing between two ranges: " << ToString();
       return 0;
     }
   }
   CHECK(false) << "block_start " << old_block_start
-               << " exceeds the limit of current RangeSet: " << this->ToString();
+               << " exceeds the limit of current RangeSet: " << ToString();
   return 0;
 }
diff --git a/otautil/SysUtil.cpp b/otautil/sysutil.cpp
similarity index 89%
rename from otautil/SysUtil.cpp
rename to otautil/sysutil.cpp
index 48336ad..d8969a0 100644
--- a/otautil/SysUtil.cpp
+++ b/otautil/sysutil.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
 
 #include <errno.h>  // TEMP_FAILURE_RETRY
 #include <fcntl.h>
@@ -23,13 +23,16 @@
 #include <sys/stat.h>
 #include <sys/types.h>
 
+#include <algorithm>
 #include <string>
 #include <vector>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
+#include <android-base/properties.h>
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
+#include <cutils/android_reboot.h>
 
 bool MemMapping::MapFD(int fd) {
   struct stat sb;
@@ -201,3 +204,19 @@
   };
   ranges_.clear();
 }
+
+bool reboot(const std::string& command) {
+  std::string cmd = command;
+  if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
+    cmd += ",quiescent";
+  }
+  return android::base::SetProperty(ANDROID_RB_PROPERTY, cmd);
+}
+
+std::vector<char*> StringVectorToNullTerminatedArray(const std::vector<std::string>& args) {
+  std::vector<char*> result(args.size());
+  std::transform(args.cbegin(), args.cend(), result.begin(),
+                 [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+  result.push_back(nullptr);
+  return result;
+}
diff --git a/otautil/ThermalUtil.cpp b/otautil/thermalutil.cpp
similarity index 98%
rename from otautil/ThermalUtil.cpp
rename to otautil/thermalutil.cpp
index 5d9bd45..4660e05 100644
--- a/otautil/ThermalUtil.cpp
+++ b/otautil/thermalutil.cpp
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-#include "otautil/ThermalUtil.h"
+#include "otautil/thermalutil.h"
 
 #include <dirent.h>
 #include <stdio.h>
diff --git a/private/install.h b/private/install.h
index ef64bd4..7fdc741 100644
--- a/private/install.h
+++ b/private/install.h
@@ -23,9 +23,17 @@
 
 #include <ziparchive/zip_archive.h>
 
-// Extract the update binary from the open zip archive |zip| located at |package| to |binary_path|.
-// Store the command line that should be called into |cmd|. The |status_fd| is the file descriptor
-// the child process should use to report back the progress of the update.
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
-                          const std::string& binary_path, int retry_count, int status_fd,
+// Sets up the commands for a non-A/B update. Extracts the updater binary from the open zip archive
+// |zip| located at |package|. Stores the command line that should be called into |cmd|. The
+// |status_fd| is the file descriptor the child process should use to report back the progress of
+// the update.
+int SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int retry_count,
+                             int status_fd, std::vector<std::string>* cmd);
+
+// Sets up the commands for an A/B update. Extracts the needed entries from the open zip archive
+// |zip| located at |package|. Stores the command line that should be called into |cmd|. The
+// |status_fd| is the file descriptor the child process should use to report back the progress of
+// the update. Note that since this applies to the sideloading flow only, it takes one less
+// parameter |retry_count| than the non-A/B version.
+int SetUpAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int status_fd,
                           std::vector<std::string>* cmd);
diff --git a/recovery-persist.cpp b/recovery-persist.cpp
index dbce7ff..ebb42d2 100644
--- a/recovery-persist.cpp
+++ b/recovery-persist.cpp
@@ -35,19 +35,22 @@
 #include <string.h>
 #include <unistd.h>
 
+#include <limits>
 #include <string>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
+#include <metricslogger/metrics_logger.h>
 #include <private/android_logger.h> /* private pmsg functions */
 
-#include "rotate_logs.h"
+#include "logging.h"
+#include "otautil/parse_install_logs.h"
 
-static const char *LAST_LOG_FILE = "/data/misc/recovery/last_log";
-static const char *LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
-static const char *LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
-static const char *LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
-static const char *ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
+constexpr const char* LAST_LOG_FILE = "/data/misc/recovery/last_log";
+constexpr const char* LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
+constexpr const char* LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
+constexpr const char* LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
+constexpr const char* ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
 
 // close a file, log an error if the error indicator is set
 static void check_and_fclose(FILE *fp, const char *name) {
@@ -109,6 +112,20 @@
     return android::base::WriteStringToFile(buffer, destination.c_str());
 }
 
+// Parses the LAST_INSTALL file and reports the update metrics saved under recovery mode.
+static void report_metrics_from_last_install(const std::string& file_name) {
+  auto metrics = ParseLastInstall(file_name);
+  // TODO(xunchang) report the installation result.
+  for (const auto& [event, value] : metrics) {
+    if (value > std::numeric_limits<int>::max()) {
+      LOG(WARNING) << event << " (" << value << ") exceeds integer max.";
+    } else {
+      LOG(INFO) << "Uploading " << value << " to " << event;
+      android::metricslogger::LogHistogram(event, value);
+    }
+  }
+}
+
 int main(int argc, char **argv) {
 
     /* Is /cache a mount?, we have been delivered where we are not wanted */
@@ -138,14 +155,18 @@
     }
 
     if (has_cache) {
-        /*
-         * TBD: Future location to move content from
-         * /cache/recovery to /data/misc/recovery/
-         */
-        /* if --force-persist flag, then transfer pmsg data anyways */
-        if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
-            return 0;
-        }
+      // Collects and reports the non-a/b update metrics from last_install; and removes the file
+      // to avoid duplicate report.
+      report_metrics_from_last_install(LAST_INSTALL_FILE_IN_CACHE);
+      if (unlink(LAST_INSTALL_FILE_IN_CACHE) == -1) {
+        PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE_IN_CACHE;
+      }
+
+      // TBD: Future location to move content from /cache/recovery to /data/misc/recovery/
+      // if --force-persist flag, then transfer pmsg data anyways
+      if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
+        return 0;
+      }
     }
 
     /* Is there something in pmsg? */
@@ -157,6 +178,15 @@
     __android_log_pmsg_file_read(
         LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", logsave, NULL);
 
+    // For those device without /cache, the last_install file has been copied to
+    // /data/misc/recovery from pmsg. Looks for the sideload history only.
+    if (!has_cache) {
+      report_metrics_from_last_install(LAST_INSTALL_FILE);
+      if (unlink(LAST_INSTALL_FILE) == -1) {
+        PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE;
+      }
+    }
+
     /* Is there a last console log too? */
     if (rotated) {
         if (!access(LAST_CONSOLE_FILE, R_OK)) {
diff --git a/recovery-refresh.cpp b/recovery-refresh.cpp
index 14565d3..aee1ca5 100644
--- a/recovery-refresh.cpp
+++ b/recovery-refresh.cpp
@@ -42,7 +42,7 @@
 
 #include <private/android_logger.h> /* private pmsg functions */
 
-#include "rotate_logs.h"
+#include "logging.h"
 
 int main(int argc, char **argv) {
     static const char filter[] = "recovery/";
diff --git a/recovery.cpp b/recovery.cpp
index 07ec5cf..7cc344b 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#include "recovery.h"
+
 #include <ctype.h>
 #include <dirent.h>
 #include <errno.h>
@@ -27,15 +29,13 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/klog.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/wait.h>
-#include <time.h>
 #include <unistd.h>
 
 #include <algorithm>
-#include <chrono>
+#include <functional>
 #include <memory>
 #include <string>
 #include <vector>
@@ -48,96 +48,44 @@
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
 #include <bootloader_message/bootloader_message.h>
-#include <cutils/android_reboot.h>
 #include <cutils/properties.h> /* for property_list */
-#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>
+#include <healthhalutils/HealthHalUtils.h>
 #include <ziparchive/zip_archive.h>
 
 #include "adb_install.h"
 #include "common.h"
 #include "device.h"
+#include "fsck_unshare_blocks.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 "logging.h"
+#include "otautil/dirutil.h"
 #include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
 #include "roots.h"
-#include "rotate_logs.h"
 #include "screen_ui.h"
-#include "stub_ui.h"
 #include "ui.h"
 
-static const struct option OPTIONS[] = {
-  { "update_package", required_argument, NULL, 'u' },
-  { "retry_count", required_argument, NULL, 'n' },
-  { "wipe_data", no_argument, NULL, 'w' },
-  { "wipe_cache", no_argument, NULL, 'c' },
-  { "show_text", no_argument, NULL, 't' },
-  { "sideload", no_argument, NULL, 's' },
-  { "sideload_auto_reboot", no_argument, NULL, 'a' },
-  { "just_exit", no_argument, NULL, 'x' },
-  { "locale", required_argument, NULL, 'l' },
-  { "shutdown_after", no_argument, NULL, 'p' },
-  { "reason", required_argument, NULL, 'r' },
-  { "security", no_argument, NULL, 'e'},
-  { "wipe_ab", no_argument, NULL, 0 },
-  { "wipe_package_size", required_argument, NULL, 0 },
-  { "prompt_and_wipe_data", no_argument, NULL, 0 },
-  { NULL, 0, NULL, 0 },
-};
+static constexpr const char* CACHE_LOG_DIR = "/cache/recovery";
+static constexpr const char* COMMAND_FILE = "/cache/recovery/command";
+static constexpr const char* LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+static constexpr const char* LAST_LOG_FILE = "/cache/recovery/last_log";
+static constexpr const char* LOCALE_FILE = "/cache/recovery/last_locale";
 
-// More bootreasons can be found in "system/core/bootstat/bootstat.cpp".
-static const std::vector<std::string> bootreason_blacklist {
-  "kernel_panic",
-  "Panic",
-};
-
-static const char *CACHE_LOG_DIR = "/cache/recovery";
-static const char *COMMAND_FILE = "/cache/recovery/command";
-static const char *LOG_FILE = "/cache/recovery/log";
-static const char *LAST_INSTALL_FILE = "/cache/recovery/last_install";
-static const char *LOCALE_FILE = "/cache/recovery/last_locale";
-static const char *CONVERT_FBE_DIR = "/tmp/convert_fbe";
-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";
-static const char *LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
-static const char *LAST_LOG_FILE = "/cache/recovery/last_log";
-// We will try to apply the update package 5 times at most in case of an I/O error or
-// bspatch | imgpatch error.
-static const int RETRY_LIMIT = 4;
-static const int BATTERY_READ_TIMEOUT_IN_SEC = 10;
-// GmsCore enters recovery mode to install package when having enough battery
-// percentage. Normally, the threshold is 40% without charger and 20% with charger.
-// So we should check battery with a slightly lower limitation.
-static const int BATTERY_OK_PERCENTAGE = 20;
-static const int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
-static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
-static constexpr const char* DEFAULT_LOCALE = "en-US";
+static constexpr const char* CACHE_ROOT = "/cache";
+static constexpr const char* DATA_ROOT = "/data";
+static constexpr const char* METADATA_ROOT = "/metadata";
+static constexpr const char* SDCARD_ROOT = "/sdcard";
 
 // 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;
-
-RecoveryUI* ui = nullptr;
 bool modified_flash = false;
 std::string stage;
 const char* reason = nullptr;
-struct selabel_handle* sehandle;
 
 /*
  * The recovery tool communicates with the main system through /cache files.
@@ -147,9 +95,10 @@
  * The arguments which may be supplied in the recovery.command file:
  *   --update_package=path - verify install an OTA package file
  *   --wipe_data - erase user data (and cache), then reboot
- *   --prompt_and_wipe_data - prompt the user that data is corrupt,
- *       with their consent erase user data (and cache), then reboot
+ *   --prompt_and_wipe_data - prompt the user that data is corrupt, with their consent erase user
+ *       data (and cache), then reboot
  *   --wipe_cache - wipe cache (but not user data), then reboot
+ *   --show_text - show the recovery text menu, used by some bootloader (e.g. http://b/36872519).
  *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
  *   --just_exit - do nothing; exit and reboot
  *
@@ -184,202 +133,10 @@
  *    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 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])) {
-    mkdir_recursively(path, 0777, true, sehandle);
-  }
-  return fopen(path, mode);
-}
-
-// close a file, log an error if the error indicator is set
-static void check_and_fclose(FILE *fp, const char *name) {
-    fflush(fp);
-    if (fsync(fileno(fp)) == -1) {
-        PLOG(ERROR) << "Failed to fsync " << name;
-    }
-    if (ferror(fp)) {
-        PLOG(ERROR) << "Error in " << name;
-    }
-    fclose(fp);
-}
-
 bool is_ro_debuggable() {
     return android::base::GetBoolProperty("ro.debuggable", false);
 }
 
-bool reboot(const std::string& command) {
-    std::string cmd = command;
-    if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
-        cmd += ",quiescent";
-    }
-    return android::base::SetProperty(ANDROID_RB_PROPERTY, cmd);
-}
-
-static void redirect_stdio(const char* filename) {
-    int pipefd[2];
-    if (pipe(pipefd) == -1) {
-        PLOG(ERROR) << "pipe failed";
-
-        // Fall back to traditional logging mode without timestamps.
-        // If these fail, there's not really anywhere to complain...
-        freopen(filename, "a", stdout); setbuf(stdout, NULL);
-        freopen(filename, "a", stderr); setbuf(stderr, NULL);
-
-        return;
-    }
-
-    pid_t pid = fork();
-    if (pid == -1) {
-        PLOG(ERROR) << "fork failed";
-
-        // Fall back to traditional logging mode without timestamps.
-        // If these fail, there's not really anywhere to complain...
-        freopen(filename, "a", stdout); setbuf(stdout, NULL);
-        freopen(filename, "a", stderr); setbuf(stderr, NULL);
-
-        return;
-    }
-
-    if (pid == 0) {
-        /// Close the unused write end.
-        close(pipefd[1]);
-
-        auto start = std::chrono::steady_clock::now();
-
-        // Child logger to actually write to the log file.
-        FILE* log_fp = fopen(filename, "ae");
-        if (log_fp == nullptr) {
-            PLOG(ERROR) << "fopen \"" << filename << "\" failed";
-            close(pipefd[0]);
-            _exit(EXIT_FAILURE);
-        }
-
-        FILE* pipe_fp = fdopen(pipefd[0], "r");
-        if (pipe_fp == nullptr) {
-            PLOG(ERROR) << "fdopen failed";
-            check_and_fclose(log_fp, filename);
-            close(pipefd[0]);
-            _exit(EXIT_FAILURE);
-        }
-
-        char* line = nullptr;
-        size_t len = 0;
-        while (getline(&line, &len, pipe_fp) != -1) {
-            auto now = std::chrono::steady_clock::now();
-            double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
-                    now - start).count();
-            if (line[0] == '\n') {
-                fprintf(log_fp, "[%12.6lf]\n", duration);
-            } else {
-                fprintf(log_fp, "[%12.6lf] %s", duration, line);
-            }
-            fflush(log_fp);
-        }
-
-        PLOG(ERROR) << "getline failed";
-
-        free(line);
-        check_and_fclose(log_fp, filename);
-        close(pipefd[0]);
-        _exit(EXIT_FAILURE);
-    } else {
-        // Redirect stdout/stderr to the logger process.
-        // Close the unused read end.
-        close(pipefd[0]);
-
-        setbuf(stdout, nullptr);
-        setbuf(stderr, nullptr);
-
-        if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
-            PLOG(ERROR) << "dup2 stdout failed";
-        }
-        if (dup2(pipefd[1], STDERR_FILENO) == -1) {
-            PLOG(ERROR) << "dup2 stderr failed";
-        }
-
-        close(pipefd[1]);
-    }
-}
-
-// command line args come from, in decreasing precedence:
-//   - the actual command line
-//   - the bootloader control block (one per line, after "recovery")
-//   - the contents of COMMAND_FILE (one per line)
-static std::vector<std::string> get_args(const int argc, char** const argv) {
-  CHECK_GT(argc, 0);
-
-  bootloader_message boot = {};
-  std::string err;
-  if (!read_bootloader_message(&boot, &err)) {
-    LOG(ERROR) << err;
-    // If fails, leave a zeroed bootloader_message.
-    boot = {};
-  }
-  stage = std::string(boot.stage);
-
-  if (boot.command[0] != 0) {
-    std::string boot_command = std::string(boot.command, sizeof(boot.command));
-    LOG(INFO) << "Boot command: " << boot_command;
-  }
-
-  if (boot.status[0] != 0) {
-    std::string boot_status = std::string(boot.status, sizeof(boot.status));
-    LOG(INFO) << "Boot status: " << boot_status;
-  }
-
-  std::vector<std::string> args(argv, argv + argc);
-
-  // --- if arguments weren't supplied, look in the bootloader control block
-  if (args.size() == 1) {
-    boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
-    std::string boot_recovery(boot.recovery);
-    std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
-    if (!tokens.empty() && tokens[0] == "recovery") {
-      for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
-        // Skip empty and '\0'-filled tokens.
-        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
-      }
-      LOG(INFO) << "Got " << args.size() << " arguments from boot message";
-    } else if (boot.recovery[0] != 0) {
-      LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
-    }
-  }
-
-  // --- if that doesn't work, try the command file (if we have /cache).
-  if (args.size() == 1 && has_cache) {
-    std::string content;
-    if (ensure_path_mounted(COMMAND_FILE) == 0 &&
-        android::base::ReadFileToString(COMMAND_FILE, &content)) {
-      std::vector<std::string> tokens = android::base::Split(content, "\n");
-      // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
-      // COMMAND_FILE doesn't use filename as the first argument).
-      for (auto it = tokens.begin(); it != tokens.end(); it++) {
-        // Skip empty and '\0'-filled tokens.
-        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
-      }
-      LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
-    }
-  }
-
-  // Write the arguments (excluding the filename in args[0]) back into the
-  // bootloader control block. So the device will always boot into recovery to
-  // finish the pending work, until finish_recovery() is called.
-  std::vector<std::string> options(args.cbegin() + 1, args.cend());
-  if (!update_bootloader_message(options, &err)) {
-    LOG(ERROR) << "Failed to set BCB message: " << err;
-  }
-
-  return args;
-}
-
 // Set the BCB to reboot back into recovery (it won't resume the install from
 // sdcard though).
 static void set_sdcard_update_bootloader_message() {
@@ -390,103 +147,11 @@
   }
 }
 
-// Read from kernel log into buffer and write out to file.
-static void save_kernel_log(const char* destination) {
-    int klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
-    if (klog_buf_len <= 0) {
-        PLOG(ERROR) << "Error getting klog size";
-        return;
-    }
-
-    std::string buffer(klog_buf_len, 0);
-    int n = klogctl(KLOG_READ_ALL, &buffer[0], klog_buf_len);
-    if (n == -1) {
-        PLOG(ERROR) << "Error in reading klog";
-        return;
-    }
-    buffer.resize(n);
-    android::base::WriteStringToFile(buffer, destination);
-}
-
-// write content to the current pmsg session.
-static ssize_t __pmsg_write(const char *filename, const char *buf, size_t len) {
-    return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
-                                         filename, buf, len);
-}
-
-static void copy_log_file_to_pmsg(const char* source, const char* destination) {
-    std::string content;
-    android::base::ReadFileToString(source, &content);
-    __pmsg_write(destination, content.c_str(), content.length());
-}
-
-// How much of the temp log we have copied to the copy in cache.
-static off_t tmplog_offset = 0;
-
-static void copy_log_file(const char* source, const char* destination, bool append) {
-  FILE* dest_fp = fopen_path(destination, append ? "ae" : "we");
-  if (dest_fp == nullptr) {
-    PLOG(ERROR) << "Can't open " << destination;
-  } else {
-    FILE* source_fp = fopen(source, "re");
-    if (source_fp != nullptr) {
-      if (append) {
-        fseeko(source_fp, tmplog_offset, SEEK_SET);  // Since last write
-      }
-      char buf[4096];
-      size_t bytes;
-      while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
-        fwrite(buf, 1, bytes, dest_fp);
-      }
-      if (append) {
-        tmplog_offset = ftello(source_fp);
-      }
-      check_and_fclose(source_fp, source);
-    }
-    check_and_fclose(dest_fp, destination);
-  }
-}
-
-static void copy_logs() {
-    // We only rotate and record the log of the current session if there are
-    // actual attempts to modify the flash, such as wipes, installs from BCB
-    // or menu selections. This is to avoid unnecessary rotation (and
-    // possible deletion) of log files, if it does not do anything loggable.
-    if (!modified_flash) {
-        return;
-    }
-
-    // Always write to pmsg, this allows the OTA logs to be caught in logcat -L
-    copy_log_file_to_pmsg(TEMPORARY_LOG_FILE, LAST_LOG_FILE);
-    copy_log_file_to_pmsg(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE);
-
-    // We can do nothing for now if there's no /cache partition.
-    if (!has_cache) {
-        return;
-    }
-
-    ensure_path_mounted(LAST_LOG_FILE);
-    ensure_path_mounted(LAST_KMSG_FILE);
-    rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
-
-    // Copy logs to cache so the system can find out what happened.
-    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
-    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
-    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
-    save_kernel_log(LAST_KMSG_FILE);
-    chmod(LOG_FILE, 0600);
-    chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
-    chmod(LAST_KMSG_FILE, 0600);
-    chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
-    chmod(LAST_LOG_FILE, 0640);
-    chmod(LAST_INSTALL_FILE, 0644);
-    sync();
-}
-
 // Clear the recovery command and prepare to boot a (hopefully working) system,
 // copy our log file to cache as well (for the system to read). This function is
 // idempotent: call it as many times as you like.
 static void finish_recovery() {
+  std::string locale = ui->GetLocale();
   // Save the locale to cache, so if recovery is next started up without a '--locale' argument
   // (e.g., directly from the bootloader) it will use the last-known locale.
   if (!locale.empty() && has_cache) {
@@ -498,7 +163,7 @@
     }
   }
 
-  copy_logs();
+  copy_logs(modified_flash, has_cache);
 
   // Reset to normal system boot so recovery won't cycle indefinitely.
   std::string err;
@@ -575,18 +240,19 @@
   ensure_path_unmounted(volume);
 
   int result;
-
   if (is_data && reason && strcmp(reason, "convert_fbe") == 0) {
-    // Create convert_fbe breadcrumb file to signal to init
-    // to convert to file based encryption, not full disk encryption
+    static constexpr const char* CONVERT_FBE_DIR = "/tmp/convert_fbe";
+    static constexpr const char* CONVERT_FBE_FILE = "/tmp/convert_fbe/convert_fbe";
+    // Create convert_fbe breadcrumb file to signal init to convert to file based encryption, not
+    // full disk encryption.
     if (mkdir(CONVERT_FBE_DIR, 0700) != 0) {
-      ui->Print("Failed to make convert_fbe dir %s\n", strerror(errno));
-      return true;
+      PLOG(ERROR) << "Failed to mkdir " << CONVERT_FBE_DIR;
+      return false;
     }
     FILE* f = fopen(CONVERT_FBE_FILE, "wbe");
     if (!f) {
-      ui->Print("Failed to convert to file encryption %s\n", strerror(errno));
-      return true;
+      PLOG(ERROR) << "Failed to convert to file encryption";
+      return false;
     }
     fclose(f);
     result = format_volume(volume, CONVERT_FBE_DIR);
@@ -613,62 +279,17 @@
     // Any part of the log we'd copied to cache is now gone.
     // Reset the pointer so we copy from the beginning of the temp
     // log.
-    tmplog_offset = 0;
-    copy_logs();
+    reset_tmplog_offset();
+    copy_logs(modified_flash, has_cache);
   }
 
   return (result == 0);
 }
 
-// Display a menu with the specified 'headers' and 'items'. Device specific HandleMenuKey() may
-// return a positive number beyond the given range. Caller sets 'menu_only' to true to ensure only
-// a menu item gets selected. 'initial_selection' controls the initial cursor location. Returns the
-// (non-negative) chosen item number, or -1 if timed out waiting for input.
-static int get_menu_selection(const char* const* headers, const char* const* items, bool menu_only,
-                              int initial_selection, Device* device) {
-  // Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
-  ui->FlushKeys();
-
-  ui->StartMenu(headers, items, initial_selection);
-
-  int selected = initial_selection;
-  int chosen_item = -1;
-  while (chosen_item < 0) {
-    int key = ui->WaitKey();
-    if (key == -1) {  // WaitKey() timed out.
-      if (ui->WasTextEverVisible()) {
-        continue;
-      } else {
-        LOG(INFO) << "Timed out waiting for key input; rebooting.";
-        ui->EndMenu();
-        return -1;
-      }
-    }
-
-    bool visible = ui->IsTextVisible();
-    int action = device->HandleMenuKey(key, visible);
-
-    if (action < 0) {
-      switch (action) {
-        case Device::kHighlightUp:
-          selected = ui->SelectMenu(--selected);
-          break;
-        case Device::kHighlightDown:
-          selected = ui->SelectMenu(++selected);
-          break;
-        case Device::kInvokeItem:
-          chosen_item = selected;
-          break;
-        case Device::kNoAction:
-          break;
-      }
-    } else if (!menu_only) {
-      chosen_item = action;
-    }
-  }
-
-  ui->EndMenu();
-  return chosen_item;
+// Sets the usb config to 'state'
+bool SetUsbConfig(const std::string& state) {
+  android::base::SetProperty("sys.usb.config", state);
+  return android::base::WaitForProperty("sys.usb.state", state);
 }
 
 // Returns the selected filename, or an empty string.
@@ -682,7 +303,7 @@
   }
 
   std::vector<std::string> dirs;
-  std::vector<std::string> zips = { "../" };  // "../" is always the first entry.
+  std::vector<std::string> entries{ "../" };  // "../" is always the first entry.
 
   dirent* de;
   while ((de = readdir(d.get())) != nullptr) {
@@ -693,29 +314,30 @@
       if (name == "." || name == "..") continue;
       dirs.push_back(name + "/");
     } else if (de->d_type == DT_REG && android::base::EndsWithIgnoreCase(name, ".zip")) {
-      zips.push_back(name);
+      entries.push_back(name);
     }
   }
 
   std::sort(dirs.begin(), dirs.end());
-  std::sort(zips.begin(), zips.end());
+  std::sort(entries.begin(), entries.end());
 
-  // Append dirs to the zips list.
-  zips.insert(zips.end(), dirs.begin(), dirs.end());
+  // Append dirs to the entries list.
+  entries.insert(entries.end(), dirs.begin(), dirs.end());
 
-  const char* entries[zips.size() + 1];
-  entries[zips.size()] = nullptr;
-  for (size_t i = 0; i < zips.size(); i++) {
-    entries[i] = zips[i].c_str();
-  }
+  std::vector<std::string> headers{ "Choose a package to install:", path };
 
-  const char* headers[] = { "Choose a package to install:", path.c_str(), nullptr };
-
-  int chosen_item = 0;
+  size_t chosen_item = 0;
   while (true) {
-    chosen_item = get_menu_selection(headers, entries, true, chosen_item, device);
+    chosen_item = ui->ShowMenu(
+        headers, entries, chosen_item, true,
+        std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
 
-    const std::string& item = zips[chosen_item];
+    // Return if WaitKey() was interrupted.
+    if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+      return "";
+    }
+
+    const std::string& item = entries[chosen_item];
     if (chosen_item == 0) {
       // Go up but continue browsing (if the caller is browse_directory).
       return "";
@@ -737,15 +359,17 @@
 }
 
 static bool yes_no(Device* device, const char* question1, const char* question2) {
-    const char* headers[] = { question1, question2, NULL };
-    const char* items[] = { " No", " Yes", NULL };
+  std::vector<std::string> headers{ question1, question2 };
+  std::vector<std::string> items{ " No", " Yes" };
 
-    int chosen_item = get_menu_selection(headers, items, true, 0, device);
-    return (chosen_item == 1);
+  size_t chosen_item = ui->ShowMenu(
+      headers, items, 0, true,
+      std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+  return (chosen_item == 1);
 }
 
 static bool ask_to_wipe_data(Device* device) {
-    return yes_no(device, "Wipe all user data?", "  THIS CAN NOT BE UNDONE!");
+  return yes_no(device, "Wipe all user data?", "  THIS CAN NOT BE UNDONE!");
 }
 
 // Return true on success.
@@ -770,27 +394,38 @@
     return success;
 }
 
-static bool prompt_and_wipe_data(Device* device) {
+static InstallResult prompt_and_wipe_data(Device* device) {
   // Use a single string and let ScreenRecoveryUI handles the wrapping.
-  const char* const headers[] = {
+  std::vector<std::string> headers{
     "Can't load Android system. Your data may be corrupt. "
     "If you continue to get this message, you may need to "
     "perform a factory data reset and erase all user data "
     "stored on this device.",
-    nullptr
   };
-  const char* const items[] = {
+  // clang-format off
+  std::vector<std::string> items {
     "Try again",
     "Factory data reset",
-    NULL
   };
+  // clang-format on
   for (;;) {
-    int chosen_item = get_menu_selection(headers, items, true, 0, device);
+    size_t chosen_item = ui->ShowMenu(
+        headers, items, 0, true,
+        std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+    // If ShowMenu() returned RecoveryUI::KeyError::INTERRUPTED, WaitKey() was interrupted.
+    if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+      return INSTALL_KEY_INTERRUPTED;
+    }
     if (chosen_item != 1) {
-      return true;  // Just reboot, no wipe; not a failure, user asked for it
+      return INSTALL_SUCCESS;  // Just reboot, no wipe; not a failure, user asked for it
     }
     if (ask_to_wipe_data(device)) {
-      return wipe_data(device);
+      if (wipe_data(device)) {
+        return INSTALL_SUCCESS;
+      } else {
+        return INSTALL_ERROR;
+      }
     }
   }
 }
@@ -913,34 +548,34 @@
     return ota_type_matched && device_type_matched && (!has_serial_number || serial_number_matched);
 }
 
-// Wipe the current A/B device, with a secure wipe of all the partitions in
-// RECOVERY_WIPE.
+// Wipes the current A/B device, with a secure wipe of all the partitions in RECOVERY_WIPE.
 static bool wipe_ab_device(size_t wipe_package_size) {
-    ui->SetBackground(RecoveryUI::ERASING);
-    ui->SetProgressType(RecoveryUI::INDETERMINATE);
+  ui->SetBackground(RecoveryUI::ERASING);
+  ui->SetProgressType(RecoveryUI::INDETERMINATE);
 
-    if (!check_wipe_package(wipe_package_size)) {
-        LOG(ERROR) << "Failed to verify wipe package";
-        return false;
-    }
-    std::string partition_list;
-    if (!android::base::ReadFileToString(RECOVERY_WIPE, &partition_list)) {
-        LOG(ERROR) << "failed to read \"" << RECOVERY_WIPE << "\"";
-        return false;
+  if (!check_wipe_package(wipe_package_size)) {
+    LOG(ERROR) << "Failed to verify wipe package";
+    return false;
+  }
+  static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
+  std::string partition_list;
+  if (!android::base::ReadFileToString(RECOVERY_WIPE, &partition_list)) {
+    LOG(ERROR) << "failed to read \"" << RECOVERY_WIPE << "\"";
+    return false;
+  }
+
+  std::vector<std::string> lines = android::base::Split(partition_list, "\n");
+  for (const std::string& line : lines) {
+    std::string partition = android::base::Trim(line);
+    // Ignore '#' comment or empty lines.
+    if (android::base::StartsWith(partition, "#") || partition.empty()) {
+      continue;
     }
 
-    std::vector<std::string> lines = android::base::Split(partition_list, "\n");
-    for (const std::string& line : lines) {
-        std::string partition = android::base::Trim(line);
-        // Ignore '#' comment or empty lines.
-        if (android::base::StartsWith(partition, "#") || partition.empty()) {
-            continue;
-        }
-
-        // Proceed anyway even if it fails to wipe some partition.
-        secure_wipe_partition(partition);
-    }
-    return true;
+    // Proceed anyway even if it fails to wipe some partition.
+    secure_wipe_partition(partition);
+  }
+  return true;
 }
 
 static void choose_recovery_file(Device* device) {
@@ -966,28 +601,30 @@
     }
   } else {
     // If cache partition is not found, view /tmp/recovery.log instead.
-    if (access(TEMPORARY_LOG_FILE, R_OK) == -1) {
+    if (access(Paths::Get().temporary_log_file().c_str(), R_OK) == -1) {
       return;
     } else {
-      entries.push_back(TEMPORARY_LOG_FILE);
+      entries.push_back(Paths::Get().temporary_log_file());
     }
   }
 
   entries.push_back("Back");
 
-  std::vector<const char*> menu_entries(entries.size());
-  std::transform(entries.cbegin(), entries.cend(), menu_entries.begin(),
-                 [](const std::string& entry) { return entry.c_str(); });
-  menu_entries.push_back(nullptr);
+  std::vector<std::string> headers{ "Select file to view" };
 
-  const char* headers[] = { "Select file to view", nullptr };
-
-  int chosen_item = 0;
+  size_t chosen_item = 0;
   while (true) {
-    chosen_item = get_menu_selection(headers, menu_entries.data(), true, chosen_item, device);
+    chosen_item = ui->ShowMenu(
+        headers, entries, chosen_item, true,
+        std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+    // Handle WaitKey() interrupt.
+    if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+      break;
+    }
     if (entries[chosen_item] == "Back") break;
 
-    ui->ShowFile(entries[chosen_item].c_str());
+    ui->ShowFile(entries[chosen_item]);
   }
 }
 
@@ -1091,8 +728,7 @@
             }
         }
 
-        result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache,
-                                 TEMPORARY_INSTALL_FILE, false, 0/*retry_count*/);
+        result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0 /*retry_count*/);
         break;
     }
 
@@ -1131,12 +767,19 @@
     }
     ui->SetProgressType(RecoveryUI::EMPTY);
 
-    int chosen_item = get_menu_selection(nullptr, device->GetMenuItems(), false, 0, device);
-
+    size_t chosen_item = ui->ShowMenu(
+        {}, device->GetMenuItems(), 0, false,
+        std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+    // Handle Interrupt key
+    if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+      return Device::KEY_INTERRUPTED;
+    }
     // Device-specific code may take some action here. It may return one of the core actions
     // handled in the switch statement below.
     Device::BuiltinAction chosen_action =
-        (chosen_item == -1) ? Device::REBOOT : device->InvokeMenuItem(chosen_item);
+        (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT))
+            ? Device::REBOOT
+            : device->InvokeMenuItem(chosen_item);
 
     bool should_wipe_cache = false;
     switch (chosen_action) {
@@ -1146,6 +789,8 @@
       case Device::REBOOT:
       case Device::SHUTDOWN:
       case Device::REBOOT_BOOTLOADER:
+      case Device::ENTER_FASTBOOT:
+      case Device::ENTER_RECOVERY:
         return chosen_action;
 
       case Device::WIPE_DATA:
@@ -1169,7 +814,7 @@
         {
           bool adb = (chosen_action == Device::APPLY_ADB_SIDELOAD);
           if (adb) {
-            status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
+            status = apply_from_adb(&should_wipe_cache);
           } else {
             status = apply_from_sdcard(device, &should_wipe_cache);
           }
@@ -1183,7 +828,7 @@
           if (status != INSTALL_SUCCESS) {
             ui->SetBackground(RecoveryUI::ERROR);
             ui->Print("Installation aborted.\n");
-            copy_logs();
+            copy_logs(modified_flash, has_cache);
           } else if (!ui->IsTextVisible()) {
             return Device::NO_ACTION;  // reboot if logs aren't visible
           } else {
@@ -1202,23 +847,24 @@
 
       case Device::RUN_LOCALE_TEST: {
         ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
-        screen_ui->CheckBackgroundTextImages(locale);
+        screen_ui->CheckBackgroundTextImages();
         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
-        // work (the symlink is created through the build system). (Bug: 22855115)
+        // the system partition is mounted at /mnt/system
         if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
-          if (ensure_path_mounted_at("/", "/system_root") != -1) {
+          if (ensure_path_mounted_at("/", "/mnt/system") != -1) {
             ui->Print("Mounted /system.\n");
           }
         } else {
-          if (ensure_path_mounted("/system") != -1) {
+          if (ensure_path_mounted_at("/system", "/mnt/system") != -1) {
             ui->Print("Mounted /system.\n");
           }
         }
         break;
+
+      case Device::KEY_INTERRUPTED:
+        return Device::KEY_INTERRUPTED;
     }
   }
 }
@@ -1227,21 +873,6 @@
   printf("%s=%s\n", key, name);
 }
 
-static std::string load_locale_from_cache() {
-    if (ensure_path_mounted(LOCALE_FILE) != 0) {
-        LOG(ERROR) << "Can't mount " << LOCALE_FILE;
-        return "";
-    }
-
-    std::string content;
-    if (!android::base::ReadFileToString(LOCALE_FILE, &content)) {
-        PLOG(ERROR) << "Can't read " << LOCALE_FILE;
-        return "";
-    }
-
-    return android::base::Trim(content);
-}
-
 void ui_print(const char* format, ...) {
     std::string buffer;
     va_list ap;
@@ -1256,55 +887,31 @@
     }
 }
 
-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);
-  }
-}
-
-static bool is_battery_ok() {
+static bool is_battery_ok(int* required_battery_level) {
   using android::hardware::health::V1_0::BatteryStatus;
+  using android::hardware::health::V2_0::get_health_service;
+  using android::hardware::health::V2_0::IHealth;
   using android::hardware::health::V2_0::Result;
   using android::hardware::health::V2_0::toString;
-  using android::hardware::health::V2_0::implementation::Health;
 
-  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
-  };
+  android::sp<IHealth> health = get_health_service();
 
-  auto health =
-      android::hardware::health::V2_0::implementation::Health::initInstance(&healthd_config);
-
+  static constexpr int BATTERY_READ_TIMEOUT_IN_SEC = 10;
   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
+
+    if (health == nullptr) {
+      LOG(WARNING) << "no health implementation is found, assuming defaults";
+    } else {
+      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 &&
@@ -1312,15 +919,17 @@
 
     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
+    if (health != nullptr) {
+      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);
+    LOG(INFO) << "charge_status " << toString(charge_status) << ", charged " << charged
+              << ", status " << toString(res) << ", capacity " << 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
@@ -1337,9 +946,15 @@
     if (res != Result::SUCCESS) {
       capacity = 100;
     }
-    return (charged && capacity >= BATTERY_WITH_CHARGER_OK_PERCENTAGE) ||
-           (!charged && capacity >= BATTERY_OK_PERCENTAGE);
-    }
+
+    // GmsCore enters recovery mode to install package when having enough battery percentage.
+    // Normally, the threshold is 40% without charger and 20% with charger. So we should check
+    // battery with a slightly lower limitation.
+    static constexpr int BATTERY_OK_PERCENTAGE = 20;
+    static constexpr int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
+    *required_battery_level = charged ? BATTERY_WITH_CHARGER_OK_PERCENTAGE : BATTERY_OK_PERCENTAGE;
+    return capacity >= *required_battery_level;
+  }
 }
 
 // Set the retry count to |retry_count| in BCB.
@@ -1362,71 +977,55 @@
 static bool bootreason_in_blacklist() {
   std::string bootreason = android::base::GetProperty("ro.boot.bootreason", "");
   if (!bootreason.empty()) {
-    for (const auto& str : bootreason_blacklist) {
-      if (strcasecmp(str.c_str(), bootreason.c_str()) == 0) {
-        return true;
-      }
+    // More bootreasons can be found in "system/core/bootstat/bootstat.cpp".
+    static const std::vector<std::string> kBootreasonBlacklist{
+      "kernel_panic",
+      "Panic",
+    };
+    for (const auto& str : kBootreasonBlacklist) {
+      if (android::base::EqualsIgnoreCase(str, bootreason)) return true;
     }
   }
   return false;
 }
 
-static void log_failure_code(ErrorCode code, const char *update_package) {
-    std::vector<std::string> log_buffer = {
-        update_package,
-        "0",  // install result
-        "error: " + std::to_string(code),
-    };
-    std::string log_content = android::base::Join(log_buffer, "\n");
-    if (!android::base::WriteStringToFile(log_content, TEMPORARY_INSTALL_FILE)) {
-        PLOG(ERROR) << "failed to write " << TEMPORARY_INSTALL_FILE;
-    }
-
-    // Also write the info into last_log.
-    LOG(INFO) << log_content;
-}
-
-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);
-
-  // Take last pmsg contents and rewrite it to the current pmsg session.
-  static const char filter[] = "recovery/";
-  // Do we need to rotate?
-  bool doRotate = false;
-
-  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &doRotate);
-  // Take action to refresh pmsg contents
-  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &doRotate);
-
-  // If this binary is started with the single argument "--adbd",
-  // instead of being the normal recovery binary, it turns into kind
-  // of a stripped-down version of adbd that only supports the
-  // 'sideload' command.  Note this must be a real argument, not
-  // anything in the command file or bootloader control block; the
-  // only way recovery should be run with this argument is when it
-  // starts a copy of itself from the apply_from_adb() function.
-  if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
-    minadbd_main();
-    return 0;
+static void log_failure_code(ErrorCode code, const std::string& update_package) {
+  std::vector<std::string> log_buffer = {
+    update_package,
+    "0",  // install result
+    "error: " + std::to_string(code),
+  };
+  std::string log_content = android::base::Join(log_buffer, "\n");
+  const std::string& install_file = Paths::Get().temporary_install_file();
+  if (!android::base::WriteStringToFile(log_content, install_file)) {
+    PLOG(ERROR) << "Failed to write " << install_file;
   }
 
-  time_t start = time(nullptr);
+  // Also write the info into last_log.
+  LOG(INFO) << log_content;
+}
 
-  // 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()); });
+Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args) {
+  static constexpr struct option OPTIONS[] = {
+    { "fastboot", no_argument, nullptr, 0 },
+    { "fsck_unshare_blocks", no_argument, nullptr, 0 },
+    { "just_exit", no_argument, nullptr, 'x' },
+    { "locale", required_argument, nullptr, 0 },
+    { "prompt_and_wipe_data", no_argument, nullptr, 0 },
+    { "reason", required_argument, nullptr, 0 },
+    { "retry_count", required_argument, nullptr, 0 },
+    { "security", no_argument, nullptr, 0 },
+    { "show_text", no_argument, nullptr, 't' },
+    { "shutdown_after", no_argument, nullptr, 0 },
+    { "sideload", no_argument, nullptr, 0 },
+    { "sideload_auto_reboot", no_argument, nullptr, 0 },
+    { "update_package", required_argument, nullptr, 0 },
+    { "wipe_ab", no_argument, nullptr, 0 },
+    { "wipe_cache", no_argument, nullptr, 0 },
+    { "wipe_data", no_argument, nullptr, 0 },
+    { "wipe_package_size", required_argument, nullptr, 0 },
+    { nullptr, 0, nullptr, 0 },
+  };
 
   const char* update_package = nullptr;
   bool should_wipe_data = false;
@@ -1434,64 +1033,62 @@
   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;
+  bool fsck_unshare_blocks = false;
   int retry_count = 0;
   bool security_update = false;
+  std::string locale;
+
+  auto args_to_parse = StringVectorToNullTerminatedArray(args);
 
   int arg;
   int option_index;
-  while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", OPTIONS,
+  // Parse everything before the last element (which must be a nullptr). getopt_long(3) expects a
+  // null-terminated char* array, but without counting null as an arg (i.e. argv[argc] should be
+  // nullptr).
+  while ((arg = getopt_long(args_to_parse.size() - 1, 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;
+        // Handled in recovery_main.cpp
         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);
+        if (option == "fsck_unshare_blocks") {
+          fsck_unshare_blocks = true;
+        } else if (option == "locale" || option == "fastboot") {
+          // Handled in recovery_main.cpp
         } else if (option == "prompt_and_wipe_data") {
           should_prompt_and_wipe_data = true;
+        } else if (option == "reason") {
+          reason = optarg;
+        } else if (option == "retry_count") {
+          android::base::ParseInt(optarg, &retry_count, 0);
+        } else if (option == "security") {
+          security_update = true;
+        } else if (option == "sideload") {
+          sideload = true;
+        } else if (option == "sideload_auto_reboot") {
+          sideload = true;
+          sideload_auto_reboot = true;
+        } else if (option == "shutdown_after") {
+          shutdown_after = true;
+        } else if (option == "update_package") {
+          update_package = optarg;
+        } else if (option == "wipe_ab") {
+          should_wipe_ab = true;
+        } else if (option == "wipe_cache") {
+          should_wipe_cache = true;
+        } else if (option == "wipe_data") {
+          should_wipe_data = true;
+        } else if (option == "wipe_package_size") {
+          android::base::ParseUint(optarg, &wipe_package_size);
         }
         break;
       }
@@ -1500,34 +1097,11 @@
         continue;
     }
   }
+  optind = 1;
 
-  if (locale.empty()) {
-    if (has_cache) {
-      locale = load_locale_from_cache();
-    }
-
-    if (locale.empty()) {
-      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);
 
-  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);
@@ -1537,15 +1111,12 @@
     ui->SetStage(st_cur, st_max);
   }
 
-  ui->SetBackground(RecoveryUI::NONE);
-  if (show_text) ui->ShowText(true);
+  std::vector<std::string> title_lines =
+      android::base::Split(android::base::GetProperty("ro.bootimage.build.fingerprint", ""), ":");
+  title_lines.insert(std::begin(title_lines), "Android Recovery");
+  ui->SetTitle(title_lines);
 
-  sehandle = selinux_android_file_context_handle();
-  selinux_android_set_sehandle(sehandle);
-  if (!sehandle) {
-    ui->Print("Warning: No file_contexts\n");
-  }
-
+  ui->ResetKeyInterruptStatus();
   device->StartRecovery();
 
   printf("Command:");
@@ -1566,14 +1137,15 @@
     // 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);
+    int required_battery_level;
+    if (retry_count == 0 && !is_battery_ok(&required_battery_level)) {
+      ui->Print("battery capacity is not enough for installing package: %d%% needed\n",
+                required_battery_level);
       // 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()) {
+    } else if (retry_count == 0 && 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);
@@ -1585,17 +1157,18 @@
         set_retry_bootloader_message(retry_count + 1, args);
       }
 
-      status = install_package(update_package, &should_wipe_cache, TEMPORARY_INSTALL_FILE, true,
-                               retry_count);
+      status = install_package(update_package, &should_wipe_cache, 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.
+
+        // When I/O error or bspatch/imgpatch error happens, reboot and retry installation
+        // RETRY_LIMIT times before we abandon this OTA update.
+        static constexpr int RETRY_LIMIT = 4;
         if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
-          copy_logs();
+          copy_logs(modified_flash, has_cache);
           retry_count += 1;
           set_retry_bootloader_message(retry_count, args);
           // Print retry count on screen.
@@ -1623,12 +1196,15 @@
       status = INSTALL_ERROR;
     }
   } else if (should_prompt_and_wipe_data) {
+    // Trigger the logging to capture the cause, even if user chooses to not wipe data.
+    modified_flash = true;
+
     ui->ShowText(true);
     ui->SetBackground(RecoveryUI::ERROR);
-    if (!prompt_and_wipe_data(device)) {
-      status = INSTALL_ERROR;
+    status = prompt_and_wipe_data(device);
+    if (status != INSTALL_KEY_INTERRUPTED) {
+      ui->ShowText(false);
     }
-    ui->ShowText(false);
   } else if (should_wipe_cache) {
     if (!wipe_cache(false, device)) {
       status = INSTALL_ERROR;
@@ -1647,7 +1223,7 @@
     if (!sideload_auto_reboot) {
       ui->ShowText(true);
     }
-    status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
+    status = apply_from_adb(&should_wipe_cache);
     if (status == INSTALL_SUCCESS && should_wipe_cache) {
       if (!wipe_cache(false, device)) {
         status = INSTALL_ERROR;
@@ -1657,6 +1233,10 @@
     if (sideload_auto_reboot) {
       ui->Print("Rebooting automatically.\n");
     }
+  } else if (fsck_unshare_blocks) {
+    if (!do_fsck_unshare_blocks()) {
+      status = INSTALL_ERROR;
+    }
   } 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
@@ -1693,25 +1273,5 @@
   // 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;
+  return after;
 }
diff --git a/mounts.h b/recovery.h
similarity index 67%
copy from mounts.h
copy to recovery.h
index 0de1ebd..00e22da 100644
--- a/mounts.h
+++ b/recovery.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * 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.
@@ -14,15 +14,11 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
 
-struct MountedVolume;
+#include <string>
+#include <vector>
 
-bool scan_mounted_volumes();
+#include "device.h"
 
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args);
diff --git a/recovery_main.cpp b/recovery_main.cpp
new file mode 100644
index 0000000..7835094
--- /dev/null
+++ b/recovery_main.cpp
@@ -0,0 +1,499 @@
+/*
+ * 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 <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atomic>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <bootloader_message/bootloader_message.h>
+#include <cutils/android_reboot.h>
+#include <cutils/sockets.h>
+#include <private/android_logger.h> /* private pmsg functions */
+#include <selinux/android.h>
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#include "common.h"
+#include "device.h"
+#include "fastboot/fastboot.h"
+#include "logging.h"
+#include "minadbd/minadbd.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
+#include "recovery.h"
+#include "roots.h"
+#include "stub_ui.h"
+#include "ui.h"
+
+static constexpr const char* COMMAND_FILE = "/cache/recovery/command";
+static constexpr const char* LOCALE_FILE = "/cache/recovery/last_locale";
+
+static constexpr const char* CACHE_ROOT = "/cache";
+
+bool has_cache = false;
+
+RecoveryUI* ui = nullptr;
+struct selabel_handle* sehandle;
+
+static void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
+                     const char* /* tag */, const char* /* file */, unsigned int /* line */,
+                     const char* message) {
+  static constexpr char log_characters[] = "VDIWEF";
+  if (severity >= android::base::ERROR && ui != nullptr) {
+    ui->Print("E:%s\n", message);
+  } else {
+    fprintf(stdout, "%c:%s\n", log_characters[severity], message);
+  }
+}
+
+// command line args come from, in decreasing precedence:
+//   - the actual command line
+//   - the bootloader control block (one per line, after "recovery")
+//   - the contents of COMMAND_FILE (one per line)
+static std::vector<std::string> get_args(const int argc, char** const argv) {
+  CHECK_GT(argc, 0);
+
+  bootloader_message boot = {};
+  std::string err;
+  if (!read_bootloader_message(&boot, &err)) {
+    LOG(ERROR) << err;
+    // If fails, leave a zeroed bootloader_message.
+    boot = {};
+  }
+  stage = std::string(boot.stage);
+
+  std::string boot_command;
+  if (boot.command[0] != 0) {
+    if (memchr(boot.command, '\0', sizeof(boot.command))) {
+      boot_command = std::string(boot.command);
+    } else {
+      boot_command = std::string(boot.command, sizeof(boot.command));
+    }
+    LOG(INFO) << "Boot command: " << boot_command;
+  }
+
+  if (boot.status[0] != 0) {
+    std::string boot_status = std::string(boot.status, sizeof(boot.status));
+    LOG(INFO) << "Boot status: " << boot_status;
+  }
+
+  std::vector<std::string> args(argv, argv + argc);
+
+  // --- if arguments weren't supplied, look in the bootloader control block
+  if (args.size() == 1) {
+    boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
+    std::string boot_recovery(boot.recovery);
+    std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
+    if (!tokens.empty() && tokens[0] == "recovery") {
+      for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
+        // Skip empty and '\0'-filled tokens.
+        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
+      }
+      LOG(INFO) << "Got " << args.size() << " arguments from boot message";
+    } else if (boot.recovery[0] != 0) {
+      LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
+    }
+  }
+
+  // --- if that doesn't work, try the command file (if we have /cache).
+  if (args.size() == 1 && has_cache) {
+    std::string content;
+    if (ensure_path_mounted(COMMAND_FILE) == 0 &&
+        android::base::ReadFileToString(COMMAND_FILE, &content)) {
+      std::vector<std::string> tokens = android::base::Split(content, "\n");
+      // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
+      // COMMAND_FILE doesn't use filename as the first argument).
+      for (auto it = tokens.begin(); it != tokens.end(); it++) {
+        // Skip empty and '\0'-filled tokens.
+        if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
+      }
+      LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
+    }
+  }
+
+  // Write the arguments (excluding the filename in args[0]) back into the
+  // bootloader control block. So the device will always boot into recovery to
+  // finish the pending work, until finish_recovery() is called.
+  std::vector<std::string> options(args.cbegin() + 1, args.cend());
+  if (!update_bootloader_message(options, &err)) {
+    LOG(ERROR) << "Failed to set BCB message: " << err;
+  }
+
+  // Finally, if no arguments were specified, check whether we should boot
+  // into fastboot.
+  if (args.size() == 1 && boot_command == "boot-fastboot") {
+    args.emplace_back("--fastboot");
+  }
+
+  return args;
+}
+
+static std::string load_locale_from_cache() {
+  if (ensure_path_mounted(LOCALE_FILE) != 0) {
+    LOG(ERROR) << "Can't mount " << LOCALE_FILE;
+    return "";
+  }
+
+  std::string content;
+  if (!android::base::ReadFileToString(LOCALE_FILE, &content)) {
+    PLOG(ERROR) << "Can't read " << LOCALE_FILE;
+    return "";
+  }
+
+  return android::base::Trim(content);
+}
+
+static void ListenRecoverySocket(RecoveryUI* ui, std::atomic<Device::BuiltinAction>& action) {
+  android::base::unique_fd sock_fd(android_get_control_socket("recovery"));
+  if (sock_fd < 0) {
+    PLOG(ERROR) << "Failed to open recovery socket";
+    return;
+  }
+  listen(sock_fd, 4);
+
+  while (true) {
+    android::base::unique_fd connection_fd;
+    connection_fd.reset(accept(sock_fd, nullptr, nullptr));
+    if (connection_fd < 0) {
+      PLOG(ERROR) << "Failed to accept socket connection";
+      continue;
+    }
+    char msg;
+    constexpr char kSwitchToFastboot = 'f';
+    constexpr char kSwitchToRecovery = 'r';
+    ssize_t ret = TEMP_FAILURE_RETRY(read(connection_fd, &msg, sizeof(msg)));
+    if (ret != sizeof(msg)) {
+      PLOG(ERROR) << "Couldn't read from socket";
+      continue;
+    }
+    switch (msg) {
+      case kSwitchToRecovery:
+        action = Device::BuiltinAction::ENTER_RECOVERY;
+        break;
+      case kSwitchToFastboot:
+        action = Device::BuiltinAction::ENTER_FASTBOOT;
+        break;
+      default:
+        LOG(ERROR) << "Unrecognized char from socket " << msg;
+        continue;
+    }
+    ui->InterruptKey();
+  }
+}
+
+static void redirect_stdio(const char* filename) {
+  int pipefd[2];
+  if (pipe(pipefd) == -1) {
+    PLOG(ERROR) << "pipe failed";
+
+    // Fall back to traditional logging mode without timestamps. If these fail, there's not really
+    // anywhere to complain...
+    freopen(filename, "a", stdout);
+    setbuf(stdout, nullptr);
+    freopen(filename, "a", stderr);
+    setbuf(stderr, nullptr);
+
+    return;
+  }
+
+  pid_t pid = fork();
+  if (pid == -1) {
+    PLOG(ERROR) << "fork failed";
+
+    // Fall back to traditional logging mode without timestamps. If these fail, there's not really
+    // anywhere to complain...
+    freopen(filename, "a", stdout);
+    setbuf(stdout, nullptr);
+    freopen(filename, "a", stderr);
+    setbuf(stderr, nullptr);
+
+    return;
+  }
+
+  if (pid == 0) {
+    /// Close the unused write end.
+    close(pipefd[1]);
+
+    auto start = std::chrono::steady_clock::now();
+
+    // Child logger to actually write to the log file.
+    FILE* log_fp = fopen(filename, "ae");
+    if (log_fp == nullptr) {
+      PLOG(ERROR) << "fopen \"" << filename << "\" failed";
+      close(pipefd[0]);
+      _exit(EXIT_FAILURE);
+    }
+
+    FILE* pipe_fp = fdopen(pipefd[0], "r");
+    if (pipe_fp == nullptr) {
+      PLOG(ERROR) << "fdopen failed";
+      check_and_fclose(log_fp, filename);
+      close(pipefd[0]);
+      _exit(EXIT_FAILURE);
+    }
+
+    char* line = nullptr;
+    size_t len = 0;
+    while (getline(&line, &len, pipe_fp) != -1) {
+      auto now = std::chrono::steady_clock::now();
+      double duration =
+          std::chrono::duration_cast<std::chrono::duration<double>>(now - start).count();
+      if (line[0] == '\n') {
+        fprintf(log_fp, "[%12.6lf]\n", duration);
+      } else {
+        fprintf(log_fp, "[%12.6lf] %s", duration, line);
+      }
+      fflush(log_fp);
+    }
+
+    PLOG(ERROR) << "getline failed";
+
+    free(line);
+    check_and_fclose(log_fp, filename);
+    close(pipefd[0]);
+    _exit(EXIT_FAILURE);
+  } else {
+    // Redirect stdout/stderr to the logger process. Close the unused read end.
+    close(pipefd[0]);
+
+    setbuf(stdout, nullptr);
+    setbuf(stderr, nullptr);
+
+    if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
+      PLOG(ERROR) << "dup2 stdout failed";
+    }
+    if (dup2(pipefd[1], STDERR_FILENO) == -1) {
+      PLOG(ERROR) << "dup2 stderr failed";
+    }
+
+    close(pipefd[1]);
+  }
+}
+
+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);
+
+  // Take last pmsg contents and rewrite it to the current pmsg session.
+  static constexpr const char filter[] = "recovery/";
+  // Do we need to rotate?
+  bool do_rotate = false;
+
+  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &do_rotate);
+  // Take action to refresh pmsg contents
+  __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &do_rotate);
+
+  // 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(Paths::Get().temporary_log_file().c_str());
+
+  load_volume_table();
+  has_cache = volume_for_mount_point(CACHE_ROOT) != nullptr;
+
+  std::vector<std::string> args = get_args(argc, argv);
+  auto args_to_parse = StringVectorToNullTerminatedArray(args);
+
+  static constexpr struct option OPTIONS[] = {
+    { "fastboot", no_argument, nullptr, 0 },
+    { "locale", required_argument, nullptr, 0 },
+    { "show_text", no_argument, nullptr, 't' },
+    { nullptr, 0, nullptr, 0 },
+  };
+
+  bool show_text = false;
+  bool fastboot = false;
+  std::string locale;
+
+  int arg;
+  int option_index;
+  while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,
+                            &option_index)) != -1) {
+    switch (arg) {
+      case 't':
+        show_text = true;
+        break;
+      case 0: {
+        std::string option = OPTIONS[option_index].name;
+        if (option == "locale") {
+          locale = optarg;
+        } else if (option == "fastboot" &&
+                   android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
+          fastboot = true;
+        }
+        break;
+      }
+    }
+  }
+  optind = 1;
+
+  if (locale.empty()) {
+    if (has_cache) {
+      locale = load_locale_from_cache();
+    }
+
+    if (locale.empty()) {
+      static constexpr const char* DEFAULT_LOCALE = "en-US";
+      locale = DEFAULT_LOCALE;
+    }
+  }
+
+  static constexpr const char* kDefaultLibRecoveryUIExt = "librecovery_ui_ext.so";
+  // Intentionally not calling dlclose(3) to avoid potential gotchas (e.g. `make_device` may have
+  // handed out pointers to code or static [or thread-local] data and doesn't collect them all back
+  // in on dlclose).
+  void* librecovery_ui_ext = dlopen(kDefaultLibRecoveryUIExt, RTLD_NOW);
+
+  using MakeDeviceType = decltype(&make_device);
+  MakeDeviceType make_device_func = nullptr;
+  if (librecovery_ui_ext == nullptr) {
+    printf("Failed to dlopen %s: %s\n", kDefaultLibRecoveryUIExt, dlerror());
+  } else {
+    reinterpret_cast<void*&>(make_device_func) = dlsym(librecovery_ui_ext, "make_device");
+    if (make_device_func == nullptr) {
+      printf("Failed to dlsym make_device: %s\n", dlerror());
+    }
+  }
+
+  Device* device;
+  if (make_device_func == nullptr) {
+    printf("Falling back to the default make_device() instead\n");
+    device = make_device();
+  } else {
+    printf("Loading make_device from %s\n", kDefaultLibRecoveryUIExt);
+    device = (*make_device_func)();
+  }
+
+  if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
+    printf("Quiescent recovery mode.\n");
+    device->ResetUI(new StubRecoveryUI());
+  } else {
+    if (!device->GetUI()->Init(locale)) {
+      printf("Failed to initialize UI; using stub UI instead.\n");
+      device->ResetUI(new StubRecoveryUI());
+    }
+  }
+  ui = device->GetUI();
+
+  if (!has_cache) {
+    device->RemoveMenuItemForAction(Device::WIPE_CACHE);
+  }
+
+  if (!android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
+    device->RemoveMenuItemForAction(Device::ENTER_FASTBOOT);
+  }
+
+  ui->SetBackground(RecoveryUI::NONE);
+  if (show_text) ui->ShowText(true);
+
+  LOG(INFO) << "Starting recovery (pid " << getpid() << ") on " << ctime(&start);
+  LOG(INFO) << "locale is [" << locale << "]";
+
+  sehandle = selinux_android_file_context_handle();
+  selinux_android_set_sehandle(sehandle);
+  if (!sehandle) {
+    ui->Print("Warning: No file_contexts\n");
+  }
+
+  std::atomic<Device::BuiltinAction> action;
+  std::thread listener_thread(ListenRecoverySocket, ui, std::ref(action));
+  listener_thread.detach();
+
+  while (true) {
+    std::string usb_config = fastboot ? "fastboot" : is_ro_debuggable() ? "adb" : "none";
+    std::string usb_state = android::base::GetProperty("sys.usb.state", "none");
+    if (usb_config != usb_state) {
+      if (!SetUsbConfig("none")) {
+        LOG(ERROR) << "Failed to clear USB config";
+      }
+      if (!SetUsbConfig(usb_config)) {
+        LOG(ERROR) << "Failed to set USB config to " << usb_config;
+      }
+    }
+
+    auto ret = fastboot ? StartFastboot(device, args) : start_recovery(device, args);
+
+    if (ret == Device::KEY_INTERRUPTED) {
+      ret = action.exchange(ret);
+      if (ret == Device::NO_ACTION) {
+        continue;
+      }
+    }
+    switch (ret) {
+      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;
+
+      case Device::ENTER_FASTBOOT:
+        LOG(INFO) << "Entering fastboot";
+        fastboot = true;
+        break;
+
+      case Device::ENTER_RECOVERY:
+        LOG(INFO) << "Entering recovery";
+        fastboot = false;
+        break;
+
+      default:
+        ui->Print("Rebooting...\n");
+        reboot("reboot,");
+        break;
+    }
+  }
+
+  // Should be unreachable.
+  return EXIT_SUCCESS;
+}
diff --git a/res-560dpi b/res-560dpi
deleted file mode 120000
index 1db3a2e..0000000
--- a/res-560dpi
+++ /dev/null
@@ -1 +0,0 @@
-res-xxxhdpi
\ No newline at end of file
diff --git a/res-hdpi/images/loop00000.png b/res-hdpi/images/loop00000.png
index 9e9d1e3..7e50647 100644
--- a/res-hdpi/images/loop00000.png
+++ b/res-hdpi/images/loop00000.png
Binary files differ
diff --git a/res-hdpi/images/loop00001.png b/res-hdpi/images/loop00001.png
index cd53cc6..c8362df 100644
--- a/res-hdpi/images/loop00001.png
+++ b/res-hdpi/images/loop00001.png
Binary files differ
diff --git a/res-hdpi/images/loop00002.png b/res-hdpi/images/loop00002.png
index d5b5cea..d239bd0 100644
--- a/res-hdpi/images/loop00002.png
+++ b/res-hdpi/images/loop00002.png
Binary files differ
diff --git a/res-hdpi/images/loop00003.png b/res-hdpi/images/loop00003.png
index 50e08de..f42f5c4 100644
--- a/res-hdpi/images/loop00003.png
+++ b/res-hdpi/images/loop00003.png
Binary files differ
diff --git a/res-hdpi/images/loop00004.png b/res-hdpi/images/loop00004.png
index d69f762..9614452 100644
--- a/res-hdpi/images/loop00004.png
+++ b/res-hdpi/images/loop00004.png
Binary files differ
diff --git a/res-hdpi/images/loop00005.png b/res-hdpi/images/loop00005.png
index 32d368e..b30dc98 100644
--- a/res-hdpi/images/loop00005.png
+++ b/res-hdpi/images/loop00005.png
Binary files differ
diff --git a/res-hdpi/images/loop00006.png b/res-hdpi/images/loop00006.png
index fcc750b..b54969c 100644
--- a/res-hdpi/images/loop00006.png
+++ b/res-hdpi/images/loop00006.png
Binary files differ
diff --git a/res-hdpi/images/loop00007.png b/res-hdpi/images/loop00007.png
index d37ba5c..e338574 100644
--- a/res-hdpi/images/loop00007.png
+++ b/res-hdpi/images/loop00007.png
Binary files differ
diff --git a/res-hdpi/images/loop00008.png b/res-hdpi/images/loop00008.png
index 5a16054..85fa0f3 100644
--- a/res-hdpi/images/loop00008.png
+++ b/res-hdpi/images/loop00008.png
Binary files differ
diff --git a/res-hdpi/images/loop00009.png b/res-hdpi/images/loop00009.png
index 49ede64..d3cbf51 100644
--- a/res-hdpi/images/loop00009.png
+++ b/res-hdpi/images/loop00009.png
Binary files differ
diff --git a/res-hdpi/images/loop00010.png b/res-hdpi/images/loop00010.png
index f9e219f..daacc20 100644
--- a/res-hdpi/images/loop00010.png
+++ b/res-hdpi/images/loop00010.png
Binary files differ
diff --git a/res-hdpi/images/loop00011.png b/res-hdpi/images/loop00011.png
index 3fbe0b5..11224ab 100644
--- a/res-hdpi/images/loop00011.png
+++ b/res-hdpi/images/loop00011.png
Binary files differ
diff --git a/res-hdpi/images/loop00012.png b/res-hdpi/images/loop00012.png
index 3229461..3426440 100644
--- a/res-hdpi/images/loop00012.png
+++ b/res-hdpi/images/loop00012.png
Binary files differ
diff --git a/res-hdpi/images/loop00013.png b/res-hdpi/images/loop00013.png
index 69773ec..56875ef 100644
--- a/res-hdpi/images/loop00013.png
+++ b/res-hdpi/images/loop00013.png
Binary files differ
diff --git a/res-hdpi/images/loop00014.png b/res-hdpi/images/loop00014.png
index 56c15cc..9117dd2 100644
--- a/res-hdpi/images/loop00014.png
+++ b/res-hdpi/images/loop00014.png
Binary files differ
diff --git a/res-hdpi/images/loop00015.png b/res-hdpi/images/loop00015.png
index 2612681..a0b31d1 100644
--- a/res-hdpi/images/loop00015.png
+++ b/res-hdpi/images/loop00015.png
Binary files differ
diff --git a/res-hdpi/images/loop00016.png b/res-hdpi/images/loop00016.png
index 69f632e..9eafa7a 100644
--- a/res-hdpi/images/loop00016.png
+++ b/res-hdpi/images/loop00016.png
Binary files differ
diff --git a/res-hdpi/images/loop00017.png b/res-hdpi/images/loop00017.png
index af35615..869987c 100644
--- a/res-hdpi/images/loop00017.png
+++ b/res-hdpi/images/loop00017.png
Binary files differ
diff --git a/res-hdpi/images/loop00018.png b/res-hdpi/images/loop00018.png
index 0f72ff0..0172c1e 100644
--- a/res-hdpi/images/loop00018.png
+++ b/res-hdpi/images/loop00018.png
Binary files differ
diff --git a/res-hdpi/images/loop00019.png b/res-hdpi/images/loop00019.png
index f167644..c6db029 100644
--- a/res-hdpi/images/loop00019.png
+++ b/res-hdpi/images/loop00019.png
Binary files differ
diff --git a/res-hdpi/images/loop00020.png b/res-hdpi/images/loop00020.png
index 202a0fe..89197e2 100644
--- a/res-hdpi/images/loop00020.png
+++ b/res-hdpi/images/loop00020.png
Binary files differ
diff --git a/res-hdpi/images/loop00021.png b/res-hdpi/images/loop00021.png
index 8c102d9..52ad8ca 100644
--- a/res-hdpi/images/loop00021.png
+++ b/res-hdpi/images/loop00021.png
Binary files differ
diff --git a/res-hdpi/images/loop00022.png b/res-hdpi/images/loop00022.png
index 4bde99c..8aa0f7b 100644
--- a/res-hdpi/images/loop00022.png
+++ b/res-hdpi/images/loop00022.png
Binary files differ
diff --git a/res-hdpi/images/loop00023.png b/res-hdpi/images/loop00023.png
index 350acfb..e037ef8 100644
--- a/res-hdpi/images/loop00023.png
+++ b/res-hdpi/images/loop00023.png
Binary files differ
diff --git a/res-hdpi/images/loop00024.png b/res-hdpi/images/loop00024.png
index dde1a8e..12611c3 100644
--- a/res-hdpi/images/loop00024.png
+++ b/res-hdpi/images/loop00024.png
Binary files differ
diff --git a/res-hdpi/images/loop00025.png b/res-hdpi/images/loop00025.png
index a133ebd..dcc5b83 100644
--- a/res-hdpi/images/loop00025.png
+++ b/res-hdpi/images/loop00025.png
Binary files differ
diff --git a/res-hdpi/images/loop00026.png b/res-hdpi/images/loop00026.png
index 6825ad9..c2762cd 100644
--- a/res-hdpi/images/loop00026.png
+++ b/res-hdpi/images/loop00026.png
Binary files differ
diff --git a/res-hdpi/images/loop00027.png b/res-hdpi/images/loop00027.png
index 91bf1cf..a119d2c 100644
--- a/res-hdpi/images/loop00027.png
+++ b/res-hdpi/images/loop00027.png
Binary files differ
diff --git a/res-hdpi/images/loop00028.png b/res-hdpi/images/loop00028.png
index 8cba9bb..87c1fb5 100644
--- a/res-hdpi/images/loop00028.png
+++ b/res-hdpi/images/loop00028.png
Binary files differ
diff --git a/res-hdpi/images/loop00029.png b/res-hdpi/images/loop00029.png
index bd05993..e689c96 100644
--- a/res-hdpi/images/loop00029.png
+++ b/res-hdpi/images/loop00029.png
Binary files differ
diff --git a/res-hdpi/images/loop00030.png b/res-hdpi/images/loop00030.png
index e30821a..18615f8 100644
--- a/res-hdpi/images/loop00030.png
+++ b/res-hdpi/images/loop00030.png
Binary files differ
diff --git a/res-hdpi/images/loop00031.png b/res-hdpi/images/loop00031.png
index 4019860..ecc9cb3 100644
--- a/res-hdpi/images/loop00031.png
+++ b/res-hdpi/images/loop00031.png
Binary files differ
diff --git a/res-hdpi/images/loop00032.png b/res-hdpi/images/loop00032.png
index 41832bb..d3831ef 100644
--- a/res-hdpi/images/loop00032.png
+++ b/res-hdpi/images/loop00032.png
Binary files differ
diff --git a/res-hdpi/images/loop00033.png b/res-hdpi/images/loop00033.png
index 583f19c..ce123a0 100644
--- a/res-hdpi/images/loop00033.png
+++ b/res-hdpi/images/loop00033.png
Binary files differ
diff --git a/res-hdpi/images/loop00034.png b/res-hdpi/images/loop00034.png
index bffa72b..7168fb2 100644
--- a/res-hdpi/images/loop00034.png
+++ b/res-hdpi/images/loop00034.png
Binary files differ
diff --git a/res-hdpi/images/loop00035.png b/res-hdpi/images/loop00035.png
index d65d6b4..0713635 100644
--- a/res-hdpi/images/loop00035.png
+++ b/res-hdpi/images/loop00035.png
Binary files differ
diff --git a/res-hdpi/images/loop00036.png b/res-hdpi/images/loop00036.png
index a26cda1..55358db 100644
--- a/res-hdpi/images/loop00036.png
+++ b/res-hdpi/images/loop00036.png
Binary files differ
diff --git a/res-hdpi/images/loop00037.png b/res-hdpi/images/loop00037.png
index 660530d..430876c 100644
--- a/res-hdpi/images/loop00037.png
+++ b/res-hdpi/images/loop00037.png
Binary files differ
diff --git a/res-hdpi/images/loop00038.png b/res-hdpi/images/loop00038.png
index a3c9f31..1155b00 100644
--- a/res-hdpi/images/loop00038.png
+++ b/res-hdpi/images/loop00038.png
Binary files differ
diff --git a/res-hdpi/images/loop00039.png b/res-hdpi/images/loop00039.png
index 609d8ca..ea43a89 100644
--- a/res-hdpi/images/loop00039.png
+++ b/res-hdpi/images/loop00039.png
Binary files differ
diff --git a/res-hdpi/images/loop00040.png b/res-hdpi/images/loop00040.png
index 4190444..e9bbfcc 100644
--- a/res-hdpi/images/loop00040.png
+++ b/res-hdpi/images/loop00040.png
Binary files differ
diff --git a/res-hdpi/images/loop00041.png b/res-hdpi/images/loop00041.png
index 9c3c371..421db51 100644
--- a/res-hdpi/images/loop00041.png
+++ b/res-hdpi/images/loop00041.png
Binary files differ
diff --git a/res-hdpi/images/loop00042.png b/res-hdpi/images/loop00042.png
index dd5baae..91d3845 100644
--- a/res-hdpi/images/loop00042.png
+++ b/res-hdpi/images/loop00042.png
Binary files differ
diff --git a/res-hdpi/images/loop00043.png b/res-hdpi/images/loop00043.png
index 814724e..944f579 100644
--- a/res-hdpi/images/loop00043.png
+++ b/res-hdpi/images/loop00043.png
Binary files differ
diff --git a/res-hdpi/images/loop00044.png b/res-hdpi/images/loop00044.png
index 63c7392..2f61616 100644
--- a/res-hdpi/images/loop00044.png
+++ b/res-hdpi/images/loop00044.png
Binary files differ
diff --git a/res-hdpi/images/loop00045.png b/res-hdpi/images/loop00045.png
index 5c666ef..147a4e9 100644
--- a/res-hdpi/images/loop00045.png
+++ b/res-hdpi/images/loop00045.png
Binary files differ
diff --git a/res-hdpi/images/loop00046.png b/res-hdpi/images/loop00046.png
index 6fa4667..fb3ebe0 100644
--- a/res-hdpi/images/loop00046.png
+++ b/res-hdpi/images/loop00046.png
Binary files differ
diff --git a/res-hdpi/images/loop00047.png b/res-hdpi/images/loop00047.png
index 52537ea..437a743 100644
--- a/res-hdpi/images/loop00047.png
+++ b/res-hdpi/images/loop00047.png
Binary files differ
diff --git a/res-hdpi/images/loop00048.png b/res-hdpi/images/loop00048.png
index 412fd1c..b91328e 100644
--- a/res-hdpi/images/loop00048.png
+++ b/res-hdpi/images/loop00048.png
Binary files differ
diff --git a/res-hdpi/images/loop00049.png b/res-hdpi/images/loop00049.png
index 6cc8ef0..aa3a1f8 100644
--- a/res-hdpi/images/loop00049.png
+++ b/res-hdpi/images/loop00049.png
Binary files differ
diff --git a/res-hdpi/images/loop00050.png b/res-hdpi/images/loop00050.png
index caf36c5..5687d77 100644
--- a/res-hdpi/images/loop00050.png
+++ b/res-hdpi/images/loop00050.png
Binary files differ
diff --git a/res-hdpi/images/loop00051.png b/res-hdpi/images/loop00051.png
index 1cf8fb4..f54a5c0 100644
--- a/res-hdpi/images/loop00051.png
+++ b/res-hdpi/images/loop00051.png
Binary files differ
diff --git a/res-hdpi/images/loop00052.png b/res-hdpi/images/loop00052.png
index 7ee60e8..50eaadc 100644
--- a/res-hdpi/images/loop00052.png
+++ b/res-hdpi/images/loop00052.png
Binary files differ
diff --git a/res-hdpi/images/loop00053.png b/res-hdpi/images/loop00053.png
index 691bca0..033c7cc 100644
--- a/res-hdpi/images/loop00053.png
+++ b/res-hdpi/images/loop00053.png
Binary files differ
diff --git a/res-hdpi/images/loop00054.png b/res-hdpi/images/loop00054.png
index fa8d000..3d9fd89 100644
--- a/res-hdpi/images/loop00054.png
+++ b/res-hdpi/images/loop00054.png
Binary files differ
diff --git a/res-hdpi/images/loop00055.png b/res-hdpi/images/loop00055.png
index 3b7acb0..b24dc8f 100644
--- a/res-hdpi/images/loop00055.png
+++ b/res-hdpi/images/loop00055.png
Binary files differ
diff --git a/res-hdpi/images/loop00056.png b/res-hdpi/images/loop00056.png
index 1c94d30..9ddf436 100644
--- a/res-hdpi/images/loop00056.png
+++ b/res-hdpi/images/loop00056.png
Binary files differ
diff --git a/res-hdpi/images/loop00057.png b/res-hdpi/images/loop00057.png
index 703f48e..16218ac 100644
--- a/res-hdpi/images/loop00057.png
+++ b/res-hdpi/images/loop00057.png
Binary files differ
diff --git a/res-hdpi/images/loop00058.png b/res-hdpi/images/loop00058.png
index 8dae68a..24d5ee3 100644
--- a/res-hdpi/images/loop00058.png
+++ b/res-hdpi/images/loop00058.png
Binary files differ
diff --git a/res-hdpi/images/loop00059.png b/res-hdpi/images/loop00059.png
index 1105b43..480e73e 100644
--- a/res-hdpi/images/loop00059.png
+++ b/res-hdpi/images/loop00059.png
Binary files differ
diff --git a/res-hdpi/images/loop00060.png b/res-hdpi/images/loop00060.png
index 8ae4a86..2429f93 100644
--- a/res-hdpi/images/loop00060.png
+++ b/res-hdpi/images/loop00060.png
Binary files differ
diff --git a/res-hdpi/images/loop00061.png b/res-hdpi/images/loop00061.png
index c4fca2f..ec29add 100644
--- a/res-hdpi/images/loop00061.png
+++ b/res-hdpi/images/loop00061.png
Binary files differ
diff --git a/res-hdpi/images/loop00062.png b/res-hdpi/images/loop00062.png
index d59b9d4..a1aaae0 100644
--- a/res-hdpi/images/loop00062.png
+++ b/res-hdpi/images/loop00062.png
Binary files differ
diff --git a/res-hdpi/images/loop00063.png b/res-hdpi/images/loop00063.png
index 7ac8fdf..b567c08 100644
--- a/res-hdpi/images/loop00063.png
+++ b/res-hdpi/images/loop00063.png
Binary files differ
diff --git a/res-hdpi/images/loop00064.png b/res-hdpi/images/loop00064.png
index 1fa8fe8..995ae43 100644
--- a/res-hdpi/images/loop00064.png
+++ b/res-hdpi/images/loop00064.png
Binary files differ
diff --git a/res-hdpi/images/loop00065.png b/res-hdpi/images/loop00065.png
index 542ed34..26e9063 100644
--- a/res-hdpi/images/loop00065.png
+++ b/res-hdpi/images/loop00065.png
Binary files differ
diff --git a/res-hdpi/images/loop00066.png b/res-hdpi/images/loop00066.png
index 7b6af52..7eac08c 100644
--- a/res-hdpi/images/loop00066.png
+++ b/res-hdpi/images/loop00066.png
Binary files differ
diff --git a/res-hdpi/images/loop00067.png b/res-hdpi/images/loop00067.png
index 58d4fb7..c865a9a 100644
--- a/res-hdpi/images/loop00067.png
+++ b/res-hdpi/images/loop00067.png
Binary files differ
diff --git a/res-hdpi/images/loop00068.png b/res-hdpi/images/loop00068.png
index 1f1616e..2a9012d 100644
--- a/res-hdpi/images/loop00068.png
+++ b/res-hdpi/images/loop00068.png
Binary files differ
diff --git a/res-hdpi/images/loop00069.png b/res-hdpi/images/loop00069.png
index a2dbbfa..8d5bbde 100644
--- a/res-hdpi/images/loop00069.png
+++ b/res-hdpi/images/loop00069.png
Binary files differ
diff --git a/res-hdpi/images/loop00070.png b/res-hdpi/images/loop00070.png
index 60a345f..a01082c 100644
--- a/res-hdpi/images/loop00070.png
+++ b/res-hdpi/images/loop00070.png
Binary files differ
diff --git a/res-hdpi/images/loop00071.png b/res-hdpi/images/loop00071.png
index ac44427..ec5511e 100644
--- a/res-hdpi/images/loop00071.png
+++ b/res-hdpi/images/loop00071.png
Binary files differ
diff --git a/res-hdpi/images/loop00072.png b/res-hdpi/images/loop00072.png
index a9171eb..e3bc89b 100644
--- a/res-hdpi/images/loop00072.png
+++ b/res-hdpi/images/loop00072.png
Binary files differ
diff --git a/res-hdpi/images/loop00073.png b/res-hdpi/images/loop00073.png
index 7911d32..5ca64fc 100644
--- a/res-hdpi/images/loop00073.png
+++ b/res-hdpi/images/loop00073.png
Binary files differ
diff --git a/res-hdpi/images/loop00074.png b/res-hdpi/images/loop00074.png
index dcea580..44223ea 100644
--- a/res-hdpi/images/loop00074.png
+++ b/res-hdpi/images/loop00074.png
Binary files differ
diff --git a/res-hdpi/images/loop00075.png b/res-hdpi/images/loop00075.png
index 0a7a5a5..08582df 100644
--- a/res-hdpi/images/loop00075.png
+++ b/res-hdpi/images/loop00075.png
Binary files differ
diff --git a/res-hdpi/images/loop00076.png b/res-hdpi/images/loop00076.png
index 674c9d2..f84ecae 100644
--- a/res-hdpi/images/loop00076.png
+++ b/res-hdpi/images/loop00076.png
Binary files differ
diff --git a/res-hdpi/images/loop00077.png b/res-hdpi/images/loop00077.png
index e344f47..35a737c 100644
--- a/res-hdpi/images/loop00077.png
+++ b/res-hdpi/images/loop00077.png
Binary files differ
diff --git a/res-hdpi/images/loop00078.png b/res-hdpi/images/loop00078.png
index e0968ce..a8a38e2 100644
--- a/res-hdpi/images/loop00078.png
+++ b/res-hdpi/images/loop00078.png
Binary files differ
diff --git a/res-hdpi/images/loop00079.png b/res-hdpi/images/loop00079.png
index 2ff1fb0..f5f3eb4 100644
--- a/res-hdpi/images/loop00079.png
+++ b/res-hdpi/images/loop00079.png
Binary files differ
diff --git a/res-hdpi/images/loop00080.png b/res-hdpi/images/loop00080.png
index 26de5af..27d566b 100644
--- a/res-hdpi/images/loop00080.png
+++ b/res-hdpi/images/loop00080.png
Binary files differ
diff --git a/res-hdpi/images/loop00081.png b/res-hdpi/images/loop00081.png
index 1ef6cdd..65e475c 100644
--- a/res-hdpi/images/loop00081.png
+++ b/res-hdpi/images/loop00081.png
Binary files differ
diff --git a/res-hdpi/images/loop00082.png b/res-hdpi/images/loop00082.png
index 334874f..af03a6f 100644
--- a/res-hdpi/images/loop00082.png
+++ b/res-hdpi/images/loop00082.png
Binary files differ
diff --git a/res-hdpi/images/loop00083.png b/res-hdpi/images/loop00083.png
index 3b0deb1..5b7c260 100644
--- a/res-hdpi/images/loop00083.png
+++ b/res-hdpi/images/loop00083.png
Binary files differ
diff --git a/res-hdpi/images/loop00084.png b/res-hdpi/images/loop00084.png
index 4b8494c..7b6ed28 100644
--- a/res-hdpi/images/loop00084.png
+++ b/res-hdpi/images/loop00084.png
Binary files differ
diff --git a/res-hdpi/images/loop00085.png b/res-hdpi/images/loop00085.png
index 2e57027..bee4ad3 100644
--- a/res-hdpi/images/loop00085.png
+++ b/res-hdpi/images/loop00085.png
Binary files differ
diff --git a/res-hdpi/images/loop00086.png b/res-hdpi/images/loop00086.png
index ab6f437..2117acf 100644
--- a/res-hdpi/images/loop00086.png
+++ b/res-hdpi/images/loop00086.png
Binary files differ
diff --git a/res-hdpi/images/loop00087.png b/res-hdpi/images/loop00087.png
index d6c3dcd..ecf9ce4 100644
--- a/res-hdpi/images/loop00087.png
+++ b/res-hdpi/images/loop00087.png
Binary files differ
diff --git a/res-hdpi/images/loop00088.png b/res-hdpi/images/loop00088.png
index 88b3868..ede9daa 100644
--- a/res-hdpi/images/loop00088.png
+++ b/res-hdpi/images/loop00088.png
Binary files differ
diff --git a/res-hdpi/images/loop00089.png b/res-hdpi/images/loop00089.png
index 5b4551b..18fd86f 100644
--- a/res-hdpi/images/loop00089.png
+++ b/res-hdpi/images/loop00089.png
Binary files differ
diff --git a/res-hdpi/images/loop00090.png b/res-hdpi/images/loop00090.png
index 9e9d1e3..7e50647 100644
--- a/res-hdpi/images/loop00090.png
+++ b/res-hdpi/images/loop00090.png
Binary files differ
diff --git a/res-mdpi/images/loop00000.png b/res-mdpi/images/loop00000.png
index 0e11c01..7af53f8 100644
--- a/res-mdpi/images/loop00000.png
+++ b/res-mdpi/images/loop00000.png
Binary files differ
diff --git a/res-mdpi/images/loop00001.png b/res-mdpi/images/loop00001.png
index 9d87ecc..83cefe3 100644
--- a/res-mdpi/images/loop00001.png
+++ b/res-mdpi/images/loop00001.png
Binary files differ
diff --git a/res-mdpi/images/loop00002.png b/res-mdpi/images/loop00002.png
index 4a47986..c3eaa69 100644
--- a/res-mdpi/images/loop00002.png
+++ b/res-mdpi/images/loop00002.png
Binary files differ
diff --git a/res-mdpi/images/loop00003.png b/res-mdpi/images/loop00003.png
index 5e01eab..444d2d0 100644
--- a/res-mdpi/images/loop00003.png
+++ b/res-mdpi/images/loop00003.png
Binary files differ
diff --git a/res-mdpi/images/loop00004.png b/res-mdpi/images/loop00004.png
index cebf84a..d621b66 100644
--- a/res-mdpi/images/loop00004.png
+++ b/res-mdpi/images/loop00004.png
Binary files differ
diff --git a/res-mdpi/images/loop00005.png b/res-mdpi/images/loop00005.png
index 4d0e8b0..6054213 100644
--- a/res-mdpi/images/loop00005.png
+++ b/res-mdpi/images/loop00005.png
Binary files differ
diff --git a/res-mdpi/images/loop00006.png b/res-mdpi/images/loop00006.png
index 00f9543..cb8edea 100644
--- a/res-mdpi/images/loop00006.png
+++ b/res-mdpi/images/loop00006.png
Binary files differ
diff --git a/res-mdpi/images/loop00007.png b/res-mdpi/images/loop00007.png
index 9564221..cbab8f5 100644
--- a/res-mdpi/images/loop00007.png
+++ b/res-mdpi/images/loop00007.png
Binary files differ
diff --git a/res-mdpi/images/loop00008.png b/res-mdpi/images/loop00008.png
index 8d41cc5..a085c45 100644
--- a/res-mdpi/images/loop00008.png
+++ b/res-mdpi/images/loop00008.png
Binary files differ
diff --git a/res-mdpi/images/loop00009.png b/res-mdpi/images/loop00009.png
index 2761756..15ca20c 100644
--- a/res-mdpi/images/loop00009.png
+++ b/res-mdpi/images/loop00009.png
Binary files differ
diff --git a/res-mdpi/images/loop00010.png b/res-mdpi/images/loop00010.png
index d8b4865..722292d 100644
--- a/res-mdpi/images/loop00010.png
+++ b/res-mdpi/images/loop00010.png
Binary files differ
diff --git a/res-mdpi/images/loop00011.png b/res-mdpi/images/loop00011.png
index 8442353..3bc7e3e 100644
--- a/res-mdpi/images/loop00011.png
+++ b/res-mdpi/images/loop00011.png
Binary files differ
diff --git a/res-mdpi/images/loop00012.png b/res-mdpi/images/loop00012.png
index cb986c5..b9aa25d 100644
--- a/res-mdpi/images/loop00012.png
+++ b/res-mdpi/images/loop00012.png
Binary files differ
diff --git a/res-mdpi/images/loop00013.png b/res-mdpi/images/loop00013.png
index 63b89b2..5897586 100644
--- a/res-mdpi/images/loop00013.png
+++ b/res-mdpi/images/loop00013.png
Binary files differ
diff --git a/res-mdpi/images/loop00014.png b/res-mdpi/images/loop00014.png
index 9713813..7a16b25 100644
--- a/res-mdpi/images/loop00014.png
+++ b/res-mdpi/images/loop00014.png
Binary files differ
diff --git a/res-mdpi/images/loop00015.png b/res-mdpi/images/loop00015.png
index 3f666d7..6791773 100644
--- a/res-mdpi/images/loop00015.png
+++ b/res-mdpi/images/loop00015.png
Binary files differ
diff --git a/res-mdpi/images/loop00016.png b/res-mdpi/images/loop00016.png
index 3d76b04..15dea3a 100644
--- a/res-mdpi/images/loop00016.png
+++ b/res-mdpi/images/loop00016.png
Binary files differ
diff --git a/res-mdpi/images/loop00017.png b/res-mdpi/images/loop00017.png
index 1438d77..6e6db83 100644
--- a/res-mdpi/images/loop00017.png
+++ b/res-mdpi/images/loop00017.png
Binary files differ
diff --git a/res-mdpi/images/loop00018.png b/res-mdpi/images/loop00018.png
index c285fc6..2055ea9 100644
--- a/res-mdpi/images/loop00018.png
+++ b/res-mdpi/images/loop00018.png
Binary files differ
diff --git a/res-mdpi/images/loop00019.png b/res-mdpi/images/loop00019.png
index d6969ec..0c0d030 100644
--- a/res-mdpi/images/loop00019.png
+++ b/res-mdpi/images/loop00019.png
Binary files differ
diff --git a/res-mdpi/images/loop00020.png b/res-mdpi/images/loop00020.png
index 89aa012..58446e3 100644
--- a/res-mdpi/images/loop00020.png
+++ b/res-mdpi/images/loop00020.png
Binary files differ
diff --git a/res-mdpi/images/loop00021.png b/res-mdpi/images/loop00021.png
index b0bd514..398d9cc 100644
--- a/res-mdpi/images/loop00021.png
+++ b/res-mdpi/images/loop00021.png
Binary files differ
diff --git a/res-mdpi/images/loop00022.png b/res-mdpi/images/loop00022.png
index 684d023..068e8fa 100644
--- a/res-mdpi/images/loop00022.png
+++ b/res-mdpi/images/loop00022.png
Binary files differ
diff --git a/res-mdpi/images/loop00023.png b/res-mdpi/images/loop00023.png
index d008e98..240140d 100644
--- a/res-mdpi/images/loop00023.png
+++ b/res-mdpi/images/loop00023.png
Binary files differ
diff --git a/res-mdpi/images/loop00024.png b/res-mdpi/images/loop00024.png
index 8fe2185..26f0e7a 100644
--- a/res-mdpi/images/loop00024.png
+++ b/res-mdpi/images/loop00024.png
Binary files differ
diff --git a/res-mdpi/images/loop00025.png b/res-mdpi/images/loop00025.png
index c534bbd..9dbc038 100644
--- a/res-mdpi/images/loop00025.png
+++ b/res-mdpi/images/loop00025.png
Binary files differ
diff --git a/res-mdpi/images/loop00026.png b/res-mdpi/images/loop00026.png
index 61b11b5..a5bb811 100644
--- a/res-mdpi/images/loop00026.png
+++ b/res-mdpi/images/loop00026.png
Binary files differ
diff --git a/res-mdpi/images/loop00027.png b/res-mdpi/images/loop00027.png
index 5c01dfc..d915367 100644
--- a/res-mdpi/images/loop00027.png
+++ b/res-mdpi/images/loop00027.png
Binary files differ
diff --git a/res-mdpi/images/loop00028.png b/res-mdpi/images/loop00028.png
index c3e61c0..9ea4ac5 100644
--- a/res-mdpi/images/loop00028.png
+++ b/res-mdpi/images/loop00028.png
Binary files differ
diff --git a/res-mdpi/images/loop00029.png b/res-mdpi/images/loop00029.png
index e0b23ff..a9b1251 100644
--- a/res-mdpi/images/loop00029.png
+++ b/res-mdpi/images/loop00029.png
Binary files differ
diff --git a/res-mdpi/images/loop00030.png b/res-mdpi/images/loop00030.png
index 6618ef7..e3a6a2d 100644
--- a/res-mdpi/images/loop00030.png
+++ b/res-mdpi/images/loop00030.png
Binary files differ
diff --git a/res-mdpi/images/loop00031.png b/res-mdpi/images/loop00031.png
index dfde81e..6eb2d3a 100644
--- a/res-mdpi/images/loop00031.png
+++ b/res-mdpi/images/loop00031.png
Binary files differ
diff --git a/res-mdpi/images/loop00032.png b/res-mdpi/images/loop00032.png
index dc6a01e..18c540e 100644
--- a/res-mdpi/images/loop00032.png
+++ b/res-mdpi/images/loop00032.png
Binary files differ
diff --git a/res-mdpi/images/loop00033.png b/res-mdpi/images/loop00033.png
index 86d104b..2379ae1 100644
--- a/res-mdpi/images/loop00033.png
+++ b/res-mdpi/images/loop00033.png
Binary files differ
diff --git a/res-mdpi/images/loop00034.png b/res-mdpi/images/loop00034.png
index 07a6d7c..422154e 100644
--- a/res-mdpi/images/loop00034.png
+++ b/res-mdpi/images/loop00034.png
Binary files differ
diff --git a/res-mdpi/images/loop00035.png b/res-mdpi/images/loop00035.png
index 3e5cb4e..b57702a 100644
--- a/res-mdpi/images/loop00035.png
+++ b/res-mdpi/images/loop00035.png
Binary files differ
diff --git a/res-mdpi/images/loop00036.png b/res-mdpi/images/loop00036.png
index 6ac7e35..17f415a 100644
--- a/res-mdpi/images/loop00036.png
+++ b/res-mdpi/images/loop00036.png
Binary files differ
diff --git a/res-mdpi/images/loop00037.png b/res-mdpi/images/loop00037.png
index 527c48d..0482866 100644
--- a/res-mdpi/images/loop00037.png
+++ b/res-mdpi/images/loop00037.png
Binary files differ
diff --git a/res-mdpi/images/loop00038.png b/res-mdpi/images/loop00038.png
index 41c6a03..8a54faf 100644
--- a/res-mdpi/images/loop00038.png
+++ b/res-mdpi/images/loop00038.png
Binary files differ
diff --git a/res-mdpi/images/loop00039.png b/res-mdpi/images/loop00039.png
index d24d642..58810b1 100644
--- a/res-mdpi/images/loop00039.png
+++ b/res-mdpi/images/loop00039.png
Binary files differ
diff --git a/res-mdpi/images/loop00040.png b/res-mdpi/images/loop00040.png
index f3f077f..08e97c2 100644
--- a/res-mdpi/images/loop00040.png
+++ b/res-mdpi/images/loop00040.png
Binary files differ
diff --git a/res-mdpi/images/loop00041.png b/res-mdpi/images/loop00041.png
index 33e0715..9b6f522 100644
--- a/res-mdpi/images/loop00041.png
+++ b/res-mdpi/images/loop00041.png
Binary files differ
diff --git a/res-mdpi/images/loop00042.png b/res-mdpi/images/loop00042.png
index b1ef146..3d62e33 100644
--- a/res-mdpi/images/loop00042.png
+++ b/res-mdpi/images/loop00042.png
Binary files differ
diff --git a/res-mdpi/images/loop00043.png b/res-mdpi/images/loop00043.png
index d835f33..01b0bc1 100644
--- a/res-mdpi/images/loop00043.png
+++ b/res-mdpi/images/loop00043.png
Binary files differ
diff --git a/res-mdpi/images/loop00044.png b/res-mdpi/images/loop00044.png
index 47ee00f..3f97b24 100644
--- a/res-mdpi/images/loop00044.png
+++ b/res-mdpi/images/loop00044.png
Binary files differ
diff --git a/res-mdpi/images/loop00045.png b/res-mdpi/images/loop00045.png
index 2c9dd71..91a5a01 100644
--- a/res-mdpi/images/loop00045.png
+++ b/res-mdpi/images/loop00045.png
Binary files differ
diff --git a/res-mdpi/images/loop00046.png b/res-mdpi/images/loop00046.png
index 7b0a557..ceb43bf 100644
--- a/res-mdpi/images/loop00046.png
+++ b/res-mdpi/images/loop00046.png
Binary files differ
diff --git a/res-mdpi/images/loop00047.png b/res-mdpi/images/loop00047.png
index 60368fe..1cf282f 100644
--- a/res-mdpi/images/loop00047.png
+++ b/res-mdpi/images/loop00047.png
Binary files differ
diff --git a/res-mdpi/images/loop00048.png b/res-mdpi/images/loop00048.png
index 8da21b5..4977fe0 100644
--- a/res-mdpi/images/loop00048.png
+++ b/res-mdpi/images/loop00048.png
Binary files differ
diff --git a/res-mdpi/images/loop00049.png b/res-mdpi/images/loop00049.png
index 8604a17..2172b67 100644
--- a/res-mdpi/images/loop00049.png
+++ b/res-mdpi/images/loop00049.png
Binary files differ
diff --git a/res-mdpi/images/loop00050.png b/res-mdpi/images/loop00050.png
index 230ebd9..b23e707 100644
--- a/res-mdpi/images/loop00050.png
+++ b/res-mdpi/images/loop00050.png
Binary files differ
diff --git a/res-mdpi/images/loop00051.png b/res-mdpi/images/loop00051.png
index 3165ae8..4c0d71c 100644
--- a/res-mdpi/images/loop00051.png
+++ b/res-mdpi/images/loop00051.png
Binary files differ
diff --git a/res-mdpi/images/loop00052.png b/res-mdpi/images/loop00052.png
index bf43112..0077c74 100644
--- a/res-mdpi/images/loop00052.png
+++ b/res-mdpi/images/loop00052.png
Binary files differ
diff --git a/res-mdpi/images/loop00053.png b/res-mdpi/images/loop00053.png
index 7d801fa..aa8f669 100644
--- a/res-mdpi/images/loop00053.png
+++ b/res-mdpi/images/loop00053.png
Binary files differ
diff --git a/res-mdpi/images/loop00054.png b/res-mdpi/images/loop00054.png
index f3ee246..b362d3e 100644
--- a/res-mdpi/images/loop00054.png
+++ b/res-mdpi/images/loop00054.png
Binary files differ
diff --git a/res-mdpi/images/loop00055.png b/res-mdpi/images/loop00055.png
index fb9fcff..d8220fb 100644
--- a/res-mdpi/images/loop00055.png
+++ b/res-mdpi/images/loop00055.png
Binary files differ
diff --git a/res-mdpi/images/loop00056.png b/res-mdpi/images/loop00056.png
index f6b1ee7..b8a4dd7 100644
--- a/res-mdpi/images/loop00056.png
+++ b/res-mdpi/images/loop00056.png
Binary files differ
diff --git a/res-mdpi/images/loop00057.png b/res-mdpi/images/loop00057.png
index af009d1..5874b05 100644
--- a/res-mdpi/images/loop00057.png
+++ b/res-mdpi/images/loop00057.png
Binary files differ
diff --git a/res-mdpi/images/loop00058.png b/res-mdpi/images/loop00058.png
index 1cd550a..27753e0 100644
--- a/res-mdpi/images/loop00058.png
+++ b/res-mdpi/images/loop00058.png
Binary files differ
diff --git a/res-mdpi/images/loop00059.png b/res-mdpi/images/loop00059.png
index cf8d18c..6094b99 100644
--- a/res-mdpi/images/loop00059.png
+++ b/res-mdpi/images/loop00059.png
Binary files differ
diff --git a/res-mdpi/images/loop00060.png b/res-mdpi/images/loop00060.png
index cfa5384..0d2fa78 100644
--- a/res-mdpi/images/loop00060.png
+++ b/res-mdpi/images/loop00060.png
Binary files differ
diff --git a/res-mdpi/images/loop00061.png b/res-mdpi/images/loop00061.png
index 5fcbf47..7076ed8 100644
--- a/res-mdpi/images/loop00061.png
+++ b/res-mdpi/images/loop00061.png
Binary files differ
diff --git a/res-mdpi/images/loop00062.png b/res-mdpi/images/loop00062.png
index d360d24..6901f4c 100644
--- a/res-mdpi/images/loop00062.png
+++ b/res-mdpi/images/loop00062.png
Binary files differ
diff --git a/res-mdpi/images/loop00063.png b/res-mdpi/images/loop00063.png
index 7f59a66..a7579af 100644
--- a/res-mdpi/images/loop00063.png
+++ b/res-mdpi/images/loop00063.png
Binary files differ
diff --git a/res-mdpi/images/loop00064.png b/res-mdpi/images/loop00064.png
index e02809f..8486ff1 100644
--- a/res-mdpi/images/loop00064.png
+++ b/res-mdpi/images/loop00064.png
Binary files differ
diff --git a/res-mdpi/images/loop00065.png b/res-mdpi/images/loop00065.png
index 597e796..ac4acba 100644
--- a/res-mdpi/images/loop00065.png
+++ b/res-mdpi/images/loop00065.png
Binary files differ
diff --git a/res-mdpi/images/loop00066.png b/res-mdpi/images/loop00066.png
index 3f308f0..97e28fc 100644
--- a/res-mdpi/images/loop00066.png
+++ b/res-mdpi/images/loop00066.png
Binary files differ
diff --git a/res-mdpi/images/loop00067.png b/res-mdpi/images/loop00067.png
index 6435982..7c1e232 100644
--- a/res-mdpi/images/loop00067.png
+++ b/res-mdpi/images/loop00067.png
Binary files differ
diff --git a/res-mdpi/images/loop00068.png b/res-mdpi/images/loop00068.png
index 580790b..943d5f4 100644
--- a/res-mdpi/images/loop00068.png
+++ b/res-mdpi/images/loop00068.png
Binary files differ
diff --git a/res-mdpi/images/loop00069.png b/res-mdpi/images/loop00069.png
index ae2f4e8..3bd05c7 100644
--- a/res-mdpi/images/loop00069.png
+++ b/res-mdpi/images/loop00069.png
Binary files differ
diff --git a/res-mdpi/images/loop00070.png b/res-mdpi/images/loop00070.png
index 8240391..941b6c5 100644
--- a/res-mdpi/images/loop00070.png
+++ b/res-mdpi/images/loop00070.png
Binary files differ
diff --git a/res-mdpi/images/loop00071.png b/res-mdpi/images/loop00071.png
index 03f157c..be59796 100644
--- a/res-mdpi/images/loop00071.png
+++ b/res-mdpi/images/loop00071.png
Binary files differ
diff --git a/res-mdpi/images/loop00072.png b/res-mdpi/images/loop00072.png
index b62dfd0..dc4bac9 100644
--- a/res-mdpi/images/loop00072.png
+++ b/res-mdpi/images/loop00072.png
Binary files differ
diff --git a/res-mdpi/images/loop00073.png b/res-mdpi/images/loop00073.png
index ba746f2..b9167a7 100644
--- a/res-mdpi/images/loop00073.png
+++ b/res-mdpi/images/loop00073.png
Binary files differ
diff --git a/res-mdpi/images/loop00074.png b/res-mdpi/images/loop00074.png
index bafd213..20c2ee6 100644
--- a/res-mdpi/images/loop00074.png
+++ b/res-mdpi/images/loop00074.png
Binary files differ
diff --git a/res-mdpi/images/loop00075.png b/res-mdpi/images/loop00075.png
index fe1f3a4..6597e34 100644
--- a/res-mdpi/images/loop00075.png
+++ b/res-mdpi/images/loop00075.png
Binary files differ
diff --git a/res-mdpi/images/loop00076.png b/res-mdpi/images/loop00076.png
index 49960e5..48cafc0 100644
--- a/res-mdpi/images/loop00076.png
+++ b/res-mdpi/images/loop00076.png
Binary files differ
diff --git a/res-mdpi/images/loop00077.png b/res-mdpi/images/loop00077.png
index a112cb8..037542e 100644
--- a/res-mdpi/images/loop00077.png
+++ b/res-mdpi/images/loop00077.png
Binary files differ
diff --git a/res-mdpi/images/loop00078.png b/res-mdpi/images/loop00078.png
index 5d69ab8..26c8558 100644
--- a/res-mdpi/images/loop00078.png
+++ b/res-mdpi/images/loop00078.png
Binary files differ
diff --git a/res-mdpi/images/loop00079.png b/res-mdpi/images/loop00079.png
index 31f3b55..fbf8a82 100644
--- a/res-mdpi/images/loop00079.png
+++ b/res-mdpi/images/loop00079.png
Binary files differ
diff --git a/res-mdpi/images/loop00080.png b/res-mdpi/images/loop00080.png
index 42730be..e89a8e1 100644
--- a/res-mdpi/images/loop00080.png
+++ b/res-mdpi/images/loop00080.png
Binary files differ
diff --git a/res-mdpi/images/loop00081.png b/res-mdpi/images/loop00081.png
index 5ea003e..e818680 100644
--- a/res-mdpi/images/loop00081.png
+++ b/res-mdpi/images/loop00081.png
Binary files differ
diff --git a/res-mdpi/images/loop00082.png b/res-mdpi/images/loop00082.png
index ead6359..b31a67d 100644
--- a/res-mdpi/images/loop00082.png
+++ b/res-mdpi/images/loop00082.png
Binary files differ
diff --git a/res-mdpi/images/loop00083.png b/res-mdpi/images/loop00083.png
index 1d10991..0005c17 100644
--- a/res-mdpi/images/loop00083.png
+++ b/res-mdpi/images/loop00083.png
Binary files differ
diff --git a/res-mdpi/images/loop00084.png b/res-mdpi/images/loop00084.png
index 5aafdec..3297da9 100644
--- a/res-mdpi/images/loop00084.png
+++ b/res-mdpi/images/loop00084.png
Binary files differ
diff --git a/res-mdpi/images/loop00085.png b/res-mdpi/images/loop00085.png
index 6813c33..fc6b1c5 100644
--- a/res-mdpi/images/loop00085.png
+++ b/res-mdpi/images/loop00085.png
Binary files differ
diff --git a/res-mdpi/images/loop00086.png b/res-mdpi/images/loop00086.png
index 5d63072..29567f7 100644
--- a/res-mdpi/images/loop00086.png
+++ b/res-mdpi/images/loop00086.png
Binary files differ
diff --git a/res-mdpi/images/loop00087.png b/res-mdpi/images/loop00087.png
index 9c65826..d2882a4 100644
--- a/res-mdpi/images/loop00087.png
+++ b/res-mdpi/images/loop00087.png
Binary files differ
diff --git a/res-mdpi/images/loop00088.png b/res-mdpi/images/loop00088.png
index 6cb1bf0..8587c07 100644
--- a/res-mdpi/images/loop00088.png
+++ b/res-mdpi/images/loop00088.png
Binary files differ
diff --git a/res-mdpi/images/loop00089.png b/res-mdpi/images/loop00089.png
index b3d742d..77cbcb5 100644
--- a/res-mdpi/images/loop00089.png
+++ b/res-mdpi/images/loop00089.png
Binary files differ
diff --git a/res-mdpi/images/loop00090.png b/res-mdpi/images/loop00090.png
index 0e11c01..7af53f8 100644
--- a/res-mdpi/images/loop00090.png
+++ b/res-mdpi/images/loop00090.png
Binary files differ
diff --git a/res-xhdpi/images/loop00000.png b/res-xhdpi/images/loop00000.png
index b438e9e..0b95c09 100644
--- a/res-xhdpi/images/loop00000.png
+++ b/res-xhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xhdpi/images/loop00001.png b/res-xhdpi/images/loop00001.png
index 343a185..54b1a16 100644
--- a/res-xhdpi/images/loop00001.png
+++ b/res-xhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xhdpi/images/loop00002.png b/res-xhdpi/images/loop00002.png
index aa5bc61..4b39d3f 100644
--- a/res-xhdpi/images/loop00002.png
+++ b/res-xhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xhdpi/images/loop00003.png b/res-xhdpi/images/loop00003.png
index 5385340..d2a7a7e 100644
--- a/res-xhdpi/images/loop00003.png
+++ b/res-xhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xhdpi/images/loop00004.png b/res-xhdpi/images/loop00004.png
index cdead7d..fa40d42 100644
--- a/res-xhdpi/images/loop00004.png
+++ b/res-xhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xhdpi/images/loop00005.png b/res-xhdpi/images/loop00005.png
index 8eb502f..4ebd50a 100644
--- a/res-xhdpi/images/loop00005.png
+++ b/res-xhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xhdpi/images/loop00006.png b/res-xhdpi/images/loop00006.png
index 60b0f4a..b732001 100644
--- a/res-xhdpi/images/loop00006.png
+++ b/res-xhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xhdpi/images/loop00007.png b/res-xhdpi/images/loop00007.png
index a76c588..158f244 100644
--- a/res-xhdpi/images/loop00007.png
+++ b/res-xhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xhdpi/images/loop00008.png b/res-xhdpi/images/loop00008.png
index 80e1603..71b2bd5 100644
--- a/res-xhdpi/images/loop00008.png
+++ b/res-xhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xhdpi/images/loop00009.png b/res-xhdpi/images/loop00009.png
index b8f4954..c92db12 100644
--- a/res-xhdpi/images/loop00009.png
+++ b/res-xhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xhdpi/images/loop00010.png b/res-xhdpi/images/loop00010.png
index b58d6ac..0a76222 100644
--- a/res-xhdpi/images/loop00010.png
+++ b/res-xhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xhdpi/images/loop00011.png b/res-xhdpi/images/loop00011.png
index 0b67f37..c26ed41 100644
--- a/res-xhdpi/images/loop00011.png
+++ b/res-xhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xhdpi/images/loop00012.png b/res-xhdpi/images/loop00012.png
index 234d77a..9f4405b 100644
--- a/res-xhdpi/images/loop00012.png
+++ b/res-xhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xhdpi/images/loop00013.png b/res-xhdpi/images/loop00013.png
index 13c6524..2d325e2 100644
--- a/res-xhdpi/images/loop00013.png
+++ b/res-xhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xhdpi/images/loop00014.png b/res-xhdpi/images/loop00014.png
index 92e30e3..d5328e2 100644
--- a/res-xhdpi/images/loop00014.png
+++ b/res-xhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xhdpi/images/loop00015.png b/res-xhdpi/images/loop00015.png
index 9c6076d..8c37522 100644
--- a/res-xhdpi/images/loop00015.png
+++ b/res-xhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xhdpi/images/loop00016.png b/res-xhdpi/images/loop00016.png
index 6f626c0..e3a881b 100644
--- a/res-xhdpi/images/loop00016.png
+++ b/res-xhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xhdpi/images/loop00017.png b/res-xhdpi/images/loop00017.png
index ff67d5b..8f6a906 100644
--- a/res-xhdpi/images/loop00017.png
+++ b/res-xhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xhdpi/images/loop00018.png b/res-xhdpi/images/loop00018.png
index 67b5d8f..e566b4a 100644
--- a/res-xhdpi/images/loop00018.png
+++ b/res-xhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xhdpi/images/loop00019.png b/res-xhdpi/images/loop00019.png
index 06ca980..66996a6 100644
--- a/res-xhdpi/images/loop00019.png
+++ b/res-xhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xhdpi/images/loop00020.png b/res-xhdpi/images/loop00020.png
index c2288b4..d2fdde3 100644
--- a/res-xhdpi/images/loop00020.png
+++ b/res-xhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xhdpi/images/loop00021.png b/res-xhdpi/images/loop00021.png
index ba5df46..01064ab 100644
--- a/res-xhdpi/images/loop00021.png
+++ b/res-xhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xhdpi/images/loop00022.png b/res-xhdpi/images/loop00022.png
index 2b1e947..a2de17b 100644
--- a/res-xhdpi/images/loop00022.png
+++ b/res-xhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xhdpi/images/loop00023.png b/res-xhdpi/images/loop00023.png
index 292e074..2bbea09 100644
--- a/res-xhdpi/images/loop00023.png
+++ b/res-xhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xhdpi/images/loop00024.png b/res-xhdpi/images/loop00024.png
index 11352f6..787b97f 100644
--- a/res-xhdpi/images/loop00024.png
+++ b/res-xhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xhdpi/images/loop00025.png b/res-xhdpi/images/loop00025.png
index 4212c76..7f37ed7 100644
--- a/res-xhdpi/images/loop00025.png
+++ b/res-xhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xhdpi/images/loop00026.png b/res-xhdpi/images/loop00026.png
index 774d00f..2ffa399 100644
--- a/res-xhdpi/images/loop00026.png
+++ b/res-xhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xhdpi/images/loop00027.png b/res-xhdpi/images/loop00027.png
index 1827471..0f9676c 100644
--- a/res-xhdpi/images/loop00027.png
+++ b/res-xhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xhdpi/images/loop00028.png b/res-xhdpi/images/loop00028.png
index f4e79f9..fd4b45d 100644
--- a/res-xhdpi/images/loop00028.png
+++ b/res-xhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xhdpi/images/loop00029.png b/res-xhdpi/images/loop00029.png
index 8638500..820c789 100644
--- a/res-xhdpi/images/loop00029.png
+++ b/res-xhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xhdpi/images/loop00030.png b/res-xhdpi/images/loop00030.png
index 94fd376..0307431 100644
--- a/res-xhdpi/images/loop00030.png
+++ b/res-xhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xhdpi/images/loop00031.png b/res-xhdpi/images/loop00031.png
index 441a52d..eebbc65 100644
--- a/res-xhdpi/images/loop00031.png
+++ b/res-xhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xhdpi/images/loop00032.png b/res-xhdpi/images/loop00032.png
index a10598f..b05284a 100644
--- a/res-xhdpi/images/loop00032.png
+++ b/res-xhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xhdpi/images/loop00033.png b/res-xhdpi/images/loop00033.png
index 96bf453..cf74124 100644
--- a/res-xhdpi/images/loop00033.png
+++ b/res-xhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xhdpi/images/loop00034.png b/res-xhdpi/images/loop00034.png
index 59baf8c..189b88a 100644
--- a/res-xhdpi/images/loop00034.png
+++ b/res-xhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xhdpi/images/loop00035.png b/res-xhdpi/images/loop00035.png
index 400a895..35b2fd2 100644
--- a/res-xhdpi/images/loop00035.png
+++ b/res-xhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xhdpi/images/loop00036.png b/res-xhdpi/images/loop00036.png
index fda7acc..5d156b2 100644
--- a/res-xhdpi/images/loop00036.png
+++ b/res-xhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xhdpi/images/loop00037.png b/res-xhdpi/images/loop00037.png
index d474e6f..08edb60 100644
--- a/res-xhdpi/images/loop00037.png
+++ b/res-xhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xhdpi/images/loop00038.png b/res-xhdpi/images/loop00038.png
index c5632e1..0e4cb0e 100644
--- a/res-xhdpi/images/loop00038.png
+++ b/res-xhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xhdpi/images/loop00039.png b/res-xhdpi/images/loop00039.png
index 3cf8b86..0671829 100644
--- a/res-xhdpi/images/loop00039.png
+++ b/res-xhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xhdpi/images/loop00040.png b/res-xhdpi/images/loop00040.png
index ef55a92..c0f602f 100644
--- a/res-xhdpi/images/loop00040.png
+++ b/res-xhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xhdpi/images/loop00041.png b/res-xhdpi/images/loop00041.png
index 60bf780..84928df 100644
--- a/res-xhdpi/images/loop00041.png
+++ b/res-xhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xhdpi/images/loop00042.png b/res-xhdpi/images/loop00042.png
index cee6980..131b316 100644
--- a/res-xhdpi/images/loop00042.png
+++ b/res-xhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xhdpi/images/loop00043.png b/res-xhdpi/images/loop00043.png
index fe5abc1..7ef0e8f 100644
--- a/res-xhdpi/images/loop00043.png
+++ b/res-xhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xhdpi/images/loop00044.png b/res-xhdpi/images/loop00044.png
index f33fcee..5fde006 100644
--- a/res-xhdpi/images/loop00044.png
+++ b/res-xhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xhdpi/images/loop00045.png b/res-xhdpi/images/loop00045.png
index e61b2a0..961459e 100644
--- a/res-xhdpi/images/loop00045.png
+++ b/res-xhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xhdpi/images/loop00046.png b/res-xhdpi/images/loop00046.png
index 4d919c0..a4610d6 100644
--- a/res-xhdpi/images/loop00046.png
+++ b/res-xhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xhdpi/images/loop00047.png b/res-xhdpi/images/loop00047.png
index deaf9a3..484becb 100644
--- a/res-xhdpi/images/loop00047.png
+++ b/res-xhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xhdpi/images/loop00048.png b/res-xhdpi/images/loop00048.png
index 82d8b2b..44b342d 100644
--- a/res-xhdpi/images/loop00048.png
+++ b/res-xhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xhdpi/images/loop00049.png b/res-xhdpi/images/loop00049.png
index a310cc9..7e96338 100644
--- a/res-xhdpi/images/loop00049.png
+++ b/res-xhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xhdpi/images/loop00050.png b/res-xhdpi/images/loop00050.png
index ad80230..3121bbe 100644
--- a/res-xhdpi/images/loop00050.png
+++ b/res-xhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xhdpi/images/loop00051.png b/res-xhdpi/images/loop00051.png
index 52f1ce6..2ea6784 100644
--- a/res-xhdpi/images/loop00051.png
+++ b/res-xhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xhdpi/images/loop00052.png b/res-xhdpi/images/loop00052.png
index c579e87..2b9b11a 100644
--- a/res-xhdpi/images/loop00052.png
+++ b/res-xhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xhdpi/images/loop00053.png b/res-xhdpi/images/loop00053.png
index 2c1bc91..716f8a1 100644
--- a/res-xhdpi/images/loop00053.png
+++ b/res-xhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xhdpi/images/loop00054.png b/res-xhdpi/images/loop00054.png
index 8885475..120adc3 100644
--- a/res-xhdpi/images/loop00054.png
+++ b/res-xhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xhdpi/images/loop00055.png b/res-xhdpi/images/loop00055.png
index 00d67da..257bfa4 100644
--- a/res-xhdpi/images/loop00055.png
+++ b/res-xhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xhdpi/images/loop00056.png b/res-xhdpi/images/loop00056.png
index 00ad26a..45467b7 100644
--- a/res-xhdpi/images/loop00056.png
+++ b/res-xhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xhdpi/images/loop00057.png b/res-xhdpi/images/loop00057.png
index 3511795..41a96fc 100644
--- a/res-xhdpi/images/loop00057.png
+++ b/res-xhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xhdpi/images/loop00058.png b/res-xhdpi/images/loop00058.png
index 9d28f7d..36ecf64 100644
--- a/res-xhdpi/images/loop00058.png
+++ b/res-xhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xhdpi/images/loop00059.png b/res-xhdpi/images/loop00059.png
index 776f40e..443c0ce 100644
--- a/res-xhdpi/images/loop00059.png
+++ b/res-xhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xhdpi/images/loop00060.png b/res-xhdpi/images/loop00060.png
index 7f728fc..b7487f3 100644
--- a/res-xhdpi/images/loop00060.png
+++ b/res-xhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xhdpi/images/loop00061.png b/res-xhdpi/images/loop00061.png
index deba021..638704d 100644
--- a/res-xhdpi/images/loop00061.png
+++ b/res-xhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xhdpi/images/loop00062.png b/res-xhdpi/images/loop00062.png
index e6b6184..3cdaee0 100644
--- a/res-xhdpi/images/loop00062.png
+++ b/res-xhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xhdpi/images/loop00063.png b/res-xhdpi/images/loop00063.png
index 0e590a5..c131227 100644
--- a/res-xhdpi/images/loop00063.png
+++ b/res-xhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xhdpi/images/loop00064.png b/res-xhdpi/images/loop00064.png
index c7b8102..4793c10 100644
--- a/res-xhdpi/images/loop00064.png
+++ b/res-xhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xhdpi/images/loop00065.png b/res-xhdpi/images/loop00065.png
index 2ccad25..2c8fff2 100644
--- a/res-xhdpi/images/loop00065.png
+++ b/res-xhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xhdpi/images/loop00066.png b/res-xhdpi/images/loop00066.png
index c5573b9..607fe0b 100644
--- a/res-xhdpi/images/loop00066.png
+++ b/res-xhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xhdpi/images/loop00067.png b/res-xhdpi/images/loop00067.png
index 005e9a6..3ce1005 100644
--- a/res-xhdpi/images/loop00067.png
+++ b/res-xhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xhdpi/images/loop00068.png b/res-xhdpi/images/loop00068.png
index b8d6a6a..a1a850b 100644
--- a/res-xhdpi/images/loop00068.png
+++ b/res-xhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xhdpi/images/loop00069.png b/res-xhdpi/images/loop00069.png
index 7e3ba30..54b2448 100644
--- a/res-xhdpi/images/loop00069.png
+++ b/res-xhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xhdpi/images/loop00070.png b/res-xhdpi/images/loop00070.png
index b9810b3..b2e8f58 100644
--- a/res-xhdpi/images/loop00070.png
+++ b/res-xhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xhdpi/images/loop00071.png b/res-xhdpi/images/loop00071.png
index 726030c..33c9449 100644
--- a/res-xhdpi/images/loop00071.png
+++ b/res-xhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xhdpi/images/loop00072.png b/res-xhdpi/images/loop00072.png
index 30c1e87..0684e17 100644
--- a/res-xhdpi/images/loop00072.png
+++ b/res-xhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xhdpi/images/loop00073.png b/res-xhdpi/images/loop00073.png
index 207a5ac..6f51a87 100644
--- a/res-xhdpi/images/loop00073.png
+++ b/res-xhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xhdpi/images/loop00074.png b/res-xhdpi/images/loop00074.png
index 4482b0c..73d38fd 100644
--- a/res-xhdpi/images/loop00074.png
+++ b/res-xhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xhdpi/images/loop00075.png b/res-xhdpi/images/loop00075.png
index 72afd08..a613a88 100644
--- a/res-xhdpi/images/loop00075.png
+++ b/res-xhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xhdpi/images/loop00076.png b/res-xhdpi/images/loop00076.png
index 4b66068..562fd75 100644
--- a/res-xhdpi/images/loop00076.png
+++ b/res-xhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xhdpi/images/loop00077.png b/res-xhdpi/images/loop00077.png
index a94989e..c1e4ee6 100644
--- a/res-xhdpi/images/loop00077.png
+++ b/res-xhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xhdpi/images/loop00078.png b/res-xhdpi/images/loop00078.png
index 810e223..3d77c78 100644
--- a/res-xhdpi/images/loop00078.png
+++ b/res-xhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xhdpi/images/loop00079.png b/res-xhdpi/images/loop00079.png
index 8085b25..c311148 100644
--- a/res-xhdpi/images/loop00079.png
+++ b/res-xhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xhdpi/images/loop00080.png b/res-xhdpi/images/loop00080.png
index 4aefa4c..c8e8013 100644
--- a/res-xhdpi/images/loop00080.png
+++ b/res-xhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xhdpi/images/loop00081.png b/res-xhdpi/images/loop00081.png
index c4a79fb..c3efcf6 100644
--- a/res-xhdpi/images/loop00081.png
+++ b/res-xhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xhdpi/images/loop00082.png b/res-xhdpi/images/loop00082.png
index 0fc9caa..30e70e6 100644
--- a/res-xhdpi/images/loop00082.png
+++ b/res-xhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xhdpi/images/loop00083.png b/res-xhdpi/images/loop00083.png
index f5fb15d..c984376 100644
--- a/res-xhdpi/images/loop00083.png
+++ b/res-xhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xhdpi/images/loop00084.png b/res-xhdpi/images/loop00084.png
index ada5a25..d06e97c 100644
--- a/res-xhdpi/images/loop00084.png
+++ b/res-xhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xhdpi/images/loop00085.png b/res-xhdpi/images/loop00085.png
index f05e8d6..335c58b 100644
--- a/res-xhdpi/images/loop00085.png
+++ b/res-xhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xhdpi/images/loop00086.png b/res-xhdpi/images/loop00086.png
index 28c5dfd..6fab9dc 100644
--- a/res-xhdpi/images/loop00086.png
+++ b/res-xhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xhdpi/images/loop00087.png b/res-xhdpi/images/loop00087.png
index d969905..a4da498 100644
--- a/res-xhdpi/images/loop00087.png
+++ b/res-xhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xhdpi/images/loop00088.png b/res-xhdpi/images/loop00088.png
index 6533002..b6c4fa5 100644
--- a/res-xhdpi/images/loop00088.png
+++ b/res-xhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xhdpi/images/loop00089.png b/res-xhdpi/images/loop00089.png
index 0d5cdea..cbc7c68 100644
--- a/res-xhdpi/images/loop00089.png
+++ b/res-xhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xhdpi/images/loop00090.png b/res-xhdpi/images/loop00090.png
index b438e9e..0b95c09 100644
--- a/res-xhdpi/images/loop00090.png
+++ b/res-xhdpi/images/loop00090.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00000.png b/res-xxhdpi/images/loop00000.png
index 003c2f8..f723d82 100644
--- a/res-xxhdpi/images/loop00000.png
+++ b/res-xxhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00001.png b/res-xxhdpi/images/loop00001.png
index 05de3dd..8cc111c 100644
--- a/res-xxhdpi/images/loop00001.png
+++ b/res-xxhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00002.png b/res-xxhdpi/images/loop00002.png
index 3b02547..3765cb5 100644
--- a/res-xxhdpi/images/loop00002.png
+++ b/res-xxhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00003.png b/res-xxhdpi/images/loop00003.png
index 21f0dcc..d8dcc18 100644
--- a/res-xxhdpi/images/loop00003.png
+++ b/res-xxhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00004.png b/res-xxhdpi/images/loop00004.png
index 6a8b758..ccd65f7 100644
--- a/res-xxhdpi/images/loop00004.png
+++ b/res-xxhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00005.png b/res-xxhdpi/images/loop00005.png
index a179aef..f996c12 100644
--- a/res-xxhdpi/images/loop00005.png
+++ b/res-xxhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00006.png b/res-xxhdpi/images/loop00006.png
index ef9f5e8..f5dcece 100644
--- a/res-xxhdpi/images/loop00006.png
+++ b/res-xxhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00007.png b/res-xxhdpi/images/loop00007.png
index 80a477d..87ae222 100644
--- a/res-xxhdpi/images/loop00007.png
+++ b/res-xxhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00008.png b/res-xxhdpi/images/loop00008.png
index 6c5cec0..b094a2d 100644
--- a/res-xxhdpi/images/loop00008.png
+++ b/res-xxhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00009.png b/res-xxhdpi/images/loop00009.png
index ac5dd30..88cd77b 100644
--- a/res-xxhdpi/images/loop00009.png
+++ b/res-xxhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00010.png b/res-xxhdpi/images/loop00010.png
index 18f10a1..ab3eb3f 100644
--- a/res-xxhdpi/images/loop00010.png
+++ b/res-xxhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00011.png b/res-xxhdpi/images/loop00011.png
index eac89e9..ab1c4f9 100644
--- a/res-xxhdpi/images/loop00011.png
+++ b/res-xxhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00012.png b/res-xxhdpi/images/loop00012.png
index 390f3cf..48235a2 100644
--- a/res-xxhdpi/images/loop00012.png
+++ b/res-xxhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00013.png b/res-xxhdpi/images/loop00013.png
index 18339e9..443227d 100644
--- a/res-xxhdpi/images/loop00013.png
+++ b/res-xxhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00014.png b/res-xxhdpi/images/loop00014.png
index 77b5be4..aa1361d 100644
--- a/res-xxhdpi/images/loop00014.png
+++ b/res-xxhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00015.png b/res-xxhdpi/images/loop00015.png
index 7c16937..6d1c431 100644
--- a/res-xxhdpi/images/loop00015.png
+++ b/res-xxhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00016.png b/res-xxhdpi/images/loop00016.png
index 50ea46e..c8fbb45 100644
--- a/res-xxhdpi/images/loop00016.png
+++ b/res-xxhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00017.png b/res-xxhdpi/images/loop00017.png
index 40bb9db..5f8f439 100644
--- a/res-xxhdpi/images/loop00017.png
+++ b/res-xxhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00018.png b/res-xxhdpi/images/loop00018.png
index 55b4d70..11fdb23 100644
--- a/res-xxhdpi/images/loop00018.png
+++ b/res-xxhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00019.png b/res-xxhdpi/images/loop00019.png
index a443090..fc15867 100644
--- a/res-xxhdpi/images/loop00019.png
+++ b/res-xxhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00020.png b/res-xxhdpi/images/loop00020.png
index 96e77ee..73b535f 100644
--- a/res-xxhdpi/images/loop00020.png
+++ b/res-xxhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00021.png b/res-xxhdpi/images/loop00021.png
index 35260af..b9d42c9 100644
--- a/res-xxhdpi/images/loop00021.png
+++ b/res-xxhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00022.png b/res-xxhdpi/images/loop00022.png
index 1861848..2213621 100644
--- a/res-xxhdpi/images/loop00022.png
+++ b/res-xxhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00023.png b/res-xxhdpi/images/loop00023.png
index 4b2e7da..3b261b4 100644
--- a/res-xxhdpi/images/loop00023.png
+++ b/res-xxhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00024.png b/res-xxhdpi/images/loop00024.png
index 1ffc765..df97506 100644
--- a/res-xxhdpi/images/loop00024.png
+++ b/res-xxhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00025.png b/res-xxhdpi/images/loop00025.png
index 9fb29d4..510704e 100644
--- a/res-xxhdpi/images/loop00025.png
+++ b/res-xxhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00026.png b/res-xxhdpi/images/loop00026.png
index 143def3..b4f0d17 100644
--- a/res-xxhdpi/images/loop00026.png
+++ b/res-xxhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00027.png b/res-xxhdpi/images/loop00027.png
index 623d6be..9a3ba15 100644
--- a/res-xxhdpi/images/loop00027.png
+++ b/res-xxhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00028.png b/res-xxhdpi/images/loop00028.png
index b7b43d2..d95aa72 100644
--- a/res-xxhdpi/images/loop00028.png
+++ b/res-xxhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00029.png b/res-xxhdpi/images/loop00029.png
index c9f183d..fc88bf9 100644
--- a/res-xxhdpi/images/loop00029.png
+++ b/res-xxhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00030.png b/res-xxhdpi/images/loop00030.png
index b85c7e3..d4692b6 100644
--- a/res-xxhdpi/images/loop00030.png
+++ b/res-xxhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00031.png b/res-xxhdpi/images/loop00031.png
index 4d938e2..500e671 100644
--- a/res-xxhdpi/images/loop00031.png
+++ b/res-xxhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00032.png b/res-xxhdpi/images/loop00032.png
index 0a17876..7a08acf 100644
--- a/res-xxhdpi/images/loop00032.png
+++ b/res-xxhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00033.png b/res-xxhdpi/images/loop00033.png
index c8919c3..8f78362 100644
--- a/res-xxhdpi/images/loop00033.png
+++ b/res-xxhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00034.png b/res-xxhdpi/images/loop00034.png
index 1584d5d..4c54233 100644
--- a/res-xxhdpi/images/loop00034.png
+++ b/res-xxhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00035.png b/res-xxhdpi/images/loop00035.png
index 2220cd3..9eef04e 100644
--- a/res-xxhdpi/images/loop00035.png
+++ b/res-xxhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00036.png b/res-xxhdpi/images/loop00036.png
index 97ae548..bdd243c 100644
--- a/res-xxhdpi/images/loop00036.png
+++ b/res-xxhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00037.png b/res-xxhdpi/images/loop00037.png
index 84fca97..153c03b 100644
--- a/res-xxhdpi/images/loop00037.png
+++ b/res-xxhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00038.png b/res-xxhdpi/images/loop00038.png
index bba2181..5133221 100644
--- a/res-xxhdpi/images/loop00038.png
+++ b/res-xxhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00039.png b/res-xxhdpi/images/loop00039.png
index 4659625..3c0b17d 100644
--- a/res-xxhdpi/images/loop00039.png
+++ b/res-xxhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00040.png b/res-xxhdpi/images/loop00040.png
index 6b3092a..fd5736e 100644
--- a/res-xxhdpi/images/loop00040.png
+++ b/res-xxhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00041.png b/res-xxhdpi/images/loop00041.png
index 5b3cd16..d2a7224 100644
--- a/res-xxhdpi/images/loop00041.png
+++ b/res-xxhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00042.png b/res-xxhdpi/images/loop00042.png
index dbb8a7f..806d58f 100644
--- a/res-xxhdpi/images/loop00042.png
+++ b/res-xxhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00043.png b/res-xxhdpi/images/loop00043.png
index 5824542..dc1106f 100644
--- a/res-xxhdpi/images/loop00043.png
+++ b/res-xxhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00044.png b/res-xxhdpi/images/loop00044.png
index d814246..15ecacd 100644
--- a/res-xxhdpi/images/loop00044.png
+++ b/res-xxhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00045.png b/res-xxhdpi/images/loop00045.png
index e6a8d30..23bbe3e 100644
--- a/res-xxhdpi/images/loop00045.png
+++ b/res-xxhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00046.png b/res-xxhdpi/images/loop00046.png
index 2f616bf..a0a2c06 100644
--- a/res-xxhdpi/images/loop00046.png
+++ b/res-xxhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00047.png b/res-xxhdpi/images/loop00047.png
index 39b74d9..cc2b433 100644
--- a/res-xxhdpi/images/loop00047.png
+++ b/res-xxhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00048.png b/res-xxhdpi/images/loop00048.png
index 2a94b8c..a891efb 100644
--- a/res-xxhdpi/images/loop00048.png
+++ b/res-xxhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00049.png b/res-xxhdpi/images/loop00049.png
index 6d86e2e..da850b3 100644
--- a/res-xxhdpi/images/loop00049.png
+++ b/res-xxhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00050.png b/res-xxhdpi/images/loop00050.png
index c6cb344..1e90f69 100644
--- a/res-xxhdpi/images/loop00050.png
+++ b/res-xxhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00051.png b/res-xxhdpi/images/loop00051.png
index dc510fa..909634a 100644
--- a/res-xxhdpi/images/loop00051.png
+++ b/res-xxhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00052.png b/res-xxhdpi/images/loop00052.png
index 9fdd3ad..e944653 100644
--- a/res-xxhdpi/images/loop00052.png
+++ b/res-xxhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00053.png b/res-xxhdpi/images/loop00053.png
index 8fff9cc..81780a4 100644
--- a/res-xxhdpi/images/loop00053.png
+++ b/res-xxhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00054.png b/res-xxhdpi/images/loop00054.png
index 1f9dfaf..a7a326b 100644
--- a/res-xxhdpi/images/loop00054.png
+++ b/res-xxhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00055.png b/res-xxhdpi/images/loop00055.png
index b0f6690..b887b96 100644
--- a/res-xxhdpi/images/loop00055.png
+++ b/res-xxhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00056.png b/res-xxhdpi/images/loop00056.png
index 79144d9..420e8a1 100644
--- a/res-xxhdpi/images/loop00056.png
+++ b/res-xxhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00057.png b/res-xxhdpi/images/loop00057.png
index a451181..e5a64f1 100644
--- a/res-xxhdpi/images/loop00057.png
+++ b/res-xxhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00058.png b/res-xxhdpi/images/loop00058.png
index eb6af3a..3988ee9 100644
--- a/res-xxhdpi/images/loop00058.png
+++ b/res-xxhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00059.png b/res-xxhdpi/images/loop00059.png
index d9a976d..fa08507 100644
--- a/res-xxhdpi/images/loop00059.png
+++ b/res-xxhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00060.png b/res-xxhdpi/images/loop00060.png
index 93ff5d9..b9e22ee 100644
--- a/res-xxhdpi/images/loop00060.png
+++ b/res-xxhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00061.png b/res-xxhdpi/images/loop00061.png
index 13dcd2a..e5b0123 100644
--- a/res-xxhdpi/images/loop00061.png
+++ b/res-xxhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00062.png b/res-xxhdpi/images/loop00062.png
index 1ffc8f8..1651c70 100644
--- a/res-xxhdpi/images/loop00062.png
+++ b/res-xxhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00063.png b/res-xxhdpi/images/loop00063.png
index 6ec7dae..127add2 100644
--- a/res-xxhdpi/images/loop00063.png
+++ b/res-xxhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00064.png b/res-xxhdpi/images/loop00064.png
index 3c5bcc3..2c9a42e 100644
--- a/res-xxhdpi/images/loop00064.png
+++ b/res-xxhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00065.png b/res-xxhdpi/images/loop00065.png
index 541fa88..1f44c87 100644
--- a/res-xxhdpi/images/loop00065.png
+++ b/res-xxhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00066.png b/res-xxhdpi/images/loop00066.png
index e65ca8f..c27ed05 100644
--- a/res-xxhdpi/images/loop00066.png
+++ b/res-xxhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00067.png b/res-xxhdpi/images/loop00067.png
index c93125b..50c18a3 100644
--- a/res-xxhdpi/images/loop00067.png
+++ b/res-xxhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00068.png b/res-xxhdpi/images/loop00068.png
index f7ef8e9..a6c0d3e 100644
--- a/res-xxhdpi/images/loop00068.png
+++ b/res-xxhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00069.png b/res-xxhdpi/images/loop00069.png
index e3a16c5..9c17c30 100644
--- a/res-xxhdpi/images/loop00069.png
+++ b/res-xxhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00070.png b/res-xxhdpi/images/loop00070.png
index 24cfdb1..4c7fd34 100644
--- a/res-xxhdpi/images/loop00070.png
+++ b/res-xxhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00071.png b/res-xxhdpi/images/loop00071.png
index efffad4..6fcb46d 100644
--- a/res-xxhdpi/images/loop00071.png
+++ b/res-xxhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00072.png b/res-xxhdpi/images/loop00072.png
index 63d62f3..a6cb6b7 100644
--- a/res-xxhdpi/images/loop00072.png
+++ b/res-xxhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00073.png b/res-xxhdpi/images/loop00073.png
index de0f410..d558555 100644
--- a/res-xxhdpi/images/loop00073.png
+++ b/res-xxhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00074.png b/res-xxhdpi/images/loop00074.png
index 45c9a74..90a672c 100644
--- a/res-xxhdpi/images/loop00074.png
+++ b/res-xxhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00075.png b/res-xxhdpi/images/loop00075.png
index a268937..5143985 100644
--- a/res-xxhdpi/images/loop00075.png
+++ b/res-xxhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00076.png b/res-xxhdpi/images/loop00076.png
index 9edd577..c99b7cb 100644
--- a/res-xxhdpi/images/loop00076.png
+++ b/res-xxhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00077.png b/res-xxhdpi/images/loop00077.png
index 23a7cc7..e5f1268 100644
--- a/res-xxhdpi/images/loop00077.png
+++ b/res-xxhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00078.png b/res-xxhdpi/images/loop00078.png
index 67dbf2d..da180ce 100644
--- a/res-xxhdpi/images/loop00078.png
+++ b/res-xxhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00079.png b/res-xxhdpi/images/loop00079.png
index 0ef021f..ece8a33 100644
--- a/res-xxhdpi/images/loop00079.png
+++ b/res-xxhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00080.png b/res-xxhdpi/images/loop00080.png
index 0de307b..be89e16 100644
--- a/res-xxhdpi/images/loop00080.png
+++ b/res-xxhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00081.png b/res-xxhdpi/images/loop00081.png
index cc31e92..77e89ea 100644
--- a/res-xxhdpi/images/loop00081.png
+++ b/res-xxhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00082.png b/res-xxhdpi/images/loop00082.png
index 6809fa3..94771b2 100644
--- a/res-xxhdpi/images/loop00082.png
+++ b/res-xxhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00083.png b/res-xxhdpi/images/loop00083.png
index c3e3a58..b76e8f5 100644
--- a/res-xxhdpi/images/loop00083.png
+++ b/res-xxhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00084.png b/res-xxhdpi/images/loop00084.png
index fc0df35..987fc13 100644
--- a/res-xxhdpi/images/loop00084.png
+++ b/res-xxhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00085.png b/res-xxhdpi/images/loop00085.png
index 38baf7e..6bb681f 100644
--- a/res-xxhdpi/images/loop00085.png
+++ b/res-xxhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00086.png b/res-xxhdpi/images/loop00086.png
index c6616eb..d31d18a 100644
--- a/res-xxhdpi/images/loop00086.png
+++ b/res-xxhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00087.png b/res-xxhdpi/images/loop00087.png
index 2e6b715..797fe7c 100644
--- a/res-xxhdpi/images/loop00087.png
+++ b/res-xxhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00088.png b/res-xxhdpi/images/loop00088.png
index 660d0df..a9764c6 100644
--- a/res-xxhdpi/images/loop00088.png
+++ b/res-xxhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00089.png b/res-xxhdpi/images/loop00089.png
index a6b82c5..b0384ec 100644
--- a/res-xxhdpi/images/loop00089.png
+++ b/res-xxhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00090.png b/res-xxhdpi/images/loop00090.png
index 003c2f8..f723d82 100644
--- a/res-xxhdpi/images/loop00090.png
+++ b/res-xxhdpi/images/loop00090.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00000.png b/res-xxxhdpi/images/loop00000.png
index d6640c5..3bca599 100644
--- a/res-xxxhdpi/images/loop00000.png
+++ b/res-xxxhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00001.png b/res-xxxhdpi/images/loop00001.png
index e1b82b9..665cd8f 100644
--- a/res-xxxhdpi/images/loop00001.png
+++ b/res-xxxhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00002.png b/res-xxxhdpi/images/loop00002.png
index 9b8a381..4f87850 100644
--- a/res-xxxhdpi/images/loop00002.png
+++ b/res-xxxhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00003.png b/res-xxxhdpi/images/loop00003.png
index b4d244c..88bdf2d 100644
--- a/res-xxxhdpi/images/loop00003.png
+++ b/res-xxxhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00004.png b/res-xxxhdpi/images/loop00004.png
index c923159..db6348e 100644
--- a/res-xxxhdpi/images/loop00004.png
+++ b/res-xxxhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00005.png b/res-xxxhdpi/images/loop00005.png
index ed739fa..4ec36c2 100644
--- a/res-xxxhdpi/images/loop00005.png
+++ b/res-xxxhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00006.png b/res-xxxhdpi/images/loop00006.png
index 6811692..a00f306 100644
--- a/res-xxxhdpi/images/loop00006.png
+++ b/res-xxxhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00007.png b/res-xxxhdpi/images/loop00007.png
index bbeee01..d140655 100644
--- a/res-xxxhdpi/images/loop00007.png
+++ b/res-xxxhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00008.png b/res-xxxhdpi/images/loop00008.png
index 2c28032..019843f 100644
--- a/res-xxxhdpi/images/loop00008.png
+++ b/res-xxxhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00009.png b/res-xxxhdpi/images/loop00009.png
index 4ea659c..5e9e7bb 100644
--- a/res-xxxhdpi/images/loop00009.png
+++ b/res-xxxhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00010.png b/res-xxxhdpi/images/loop00010.png
index 45928bc..52ae668 100644
--- a/res-xxxhdpi/images/loop00010.png
+++ b/res-xxxhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00011.png b/res-xxxhdpi/images/loop00011.png
index 8a8f2f7..bd318f6 100644
--- a/res-xxxhdpi/images/loop00011.png
+++ b/res-xxxhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00012.png b/res-xxxhdpi/images/loop00012.png
index 1714d1b..114e326 100644
--- a/res-xxxhdpi/images/loop00012.png
+++ b/res-xxxhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00013.png b/res-xxxhdpi/images/loop00013.png
index 18ab24f..8890e6d 100644
--- a/res-xxxhdpi/images/loop00013.png
+++ b/res-xxxhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00014.png b/res-xxxhdpi/images/loop00014.png
index 5099bc5..72e6b12 100644
--- a/res-xxxhdpi/images/loop00014.png
+++ b/res-xxxhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00015.png b/res-xxxhdpi/images/loop00015.png
index b7e6868..cbf0b1b 100644
--- a/res-xxxhdpi/images/loop00015.png
+++ b/res-xxxhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00016.png b/res-xxxhdpi/images/loop00016.png
index bc13375..b9f16aa 100644
--- a/res-xxxhdpi/images/loop00016.png
+++ b/res-xxxhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00017.png b/res-xxxhdpi/images/loop00017.png
index 8a9bd86..536201a 100644
--- a/res-xxxhdpi/images/loop00017.png
+++ b/res-xxxhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00018.png b/res-xxxhdpi/images/loop00018.png
index 2150d63..6ef1b32 100644
--- a/res-xxxhdpi/images/loop00018.png
+++ b/res-xxxhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00019.png b/res-xxxhdpi/images/loop00019.png
index ec0cc58..63ffe04 100644
--- a/res-xxxhdpi/images/loop00019.png
+++ b/res-xxxhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00020.png b/res-xxxhdpi/images/loop00020.png
index 6596ea2..5e28c00 100644
--- a/res-xxxhdpi/images/loop00020.png
+++ b/res-xxxhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00021.png b/res-xxxhdpi/images/loop00021.png
index c874649..0e085f4 100644
--- a/res-xxxhdpi/images/loop00021.png
+++ b/res-xxxhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00022.png b/res-xxxhdpi/images/loop00022.png
index d5f834d..9559415 100644
--- a/res-xxxhdpi/images/loop00022.png
+++ b/res-xxxhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00023.png b/res-xxxhdpi/images/loop00023.png
index eb8af82..3cc2d5c 100644
--- a/res-xxxhdpi/images/loop00023.png
+++ b/res-xxxhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00024.png b/res-xxxhdpi/images/loop00024.png
index 7da5506..fd4addc 100644
--- a/res-xxxhdpi/images/loop00024.png
+++ b/res-xxxhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00025.png b/res-xxxhdpi/images/loop00025.png
index 8844149..69fef86 100644
--- a/res-xxxhdpi/images/loop00025.png
+++ b/res-xxxhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00026.png b/res-xxxhdpi/images/loop00026.png
index ee36358..a4ff78f 100644
--- a/res-xxxhdpi/images/loop00026.png
+++ b/res-xxxhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00027.png b/res-xxxhdpi/images/loop00027.png
index 0299dae..6adc338 100644
--- a/res-xxxhdpi/images/loop00027.png
+++ b/res-xxxhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00028.png b/res-xxxhdpi/images/loop00028.png
index a8f5cef..86f2d0e 100644
--- a/res-xxxhdpi/images/loop00028.png
+++ b/res-xxxhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00029.png b/res-xxxhdpi/images/loop00029.png
index 6b2ab3f..4ce7d7c 100644
--- a/res-xxxhdpi/images/loop00029.png
+++ b/res-xxxhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00030.png b/res-xxxhdpi/images/loop00030.png
index 2d5b48d..0599603 100644
--- a/res-xxxhdpi/images/loop00030.png
+++ b/res-xxxhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00031.png b/res-xxxhdpi/images/loop00031.png
index 40c4296..7f96ed5 100644
--- a/res-xxxhdpi/images/loop00031.png
+++ b/res-xxxhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00032.png b/res-xxxhdpi/images/loop00032.png
index f130b0a..6f66c5d 100644
--- a/res-xxxhdpi/images/loop00032.png
+++ b/res-xxxhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00033.png b/res-xxxhdpi/images/loop00033.png
index 24151ba..4052bb2 100644
--- a/res-xxxhdpi/images/loop00033.png
+++ b/res-xxxhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00034.png b/res-xxxhdpi/images/loop00034.png
index f74f895..d751eef 100644
--- a/res-xxxhdpi/images/loop00034.png
+++ b/res-xxxhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00035.png b/res-xxxhdpi/images/loop00035.png
index 4a0f805..4d28591 100644
--- a/res-xxxhdpi/images/loop00035.png
+++ b/res-xxxhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00036.png b/res-xxxhdpi/images/loop00036.png
index 7465862..8f662e8 100644
--- a/res-xxxhdpi/images/loop00036.png
+++ b/res-xxxhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00037.png b/res-xxxhdpi/images/loop00037.png
index 5d10d10..de108e9 100644
--- a/res-xxxhdpi/images/loop00037.png
+++ b/res-xxxhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00038.png b/res-xxxhdpi/images/loop00038.png
index 15d5db2..812ff36 100644
--- a/res-xxxhdpi/images/loop00038.png
+++ b/res-xxxhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00039.png b/res-xxxhdpi/images/loop00039.png
index b92d49d..e139c43 100644
--- a/res-xxxhdpi/images/loop00039.png
+++ b/res-xxxhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00040.png b/res-xxxhdpi/images/loop00040.png
index 5c19c02..0fc29f1 100644
--- a/res-xxxhdpi/images/loop00040.png
+++ b/res-xxxhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00041.png b/res-xxxhdpi/images/loop00041.png
index 2c9d406..f9b582d 100644
--- a/res-xxxhdpi/images/loop00041.png
+++ b/res-xxxhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00042.png b/res-xxxhdpi/images/loop00042.png
index bb24da5..05678d7 100644
--- a/res-xxxhdpi/images/loop00042.png
+++ b/res-xxxhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00043.png b/res-xxxhdpi/images/loop00043.png
index 0a9efd8..eef9bab 100644
--- a/res-xxxhdpi/images/loop00043.png
+++ b/res-xxxhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00044.png b/res-xxxhdpi/images/loop00044.png
index 70e1cbc..9a1060e 100644
--- a/res-xxxhdpi/images/loop00044.png
+++ b/res-xxxhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00045.png b/res-xxxhdpi/images/loop00045.png
index 0ecb787..41ec4ec 100644
--- a/res-xxxhdpi/images/loop00045.png
+++ b/res-xxxhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00046.png b/res-xxxhdpi/images/loop00046.png
index c2c425a..61a87c4 100644
--- a/res-xxxhdpi/images/loop00046.png
+++ b/res-xxxhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00047.png b/res-xxxhdpi/images/loop00047.png
index 71812b3..1bb7e86 100644
--- a/res-xxxhdpi/images/loop00047.png
+++ b/res-xxxhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00048.png b/res-xxxhdpi/images/loop00048.png
index 6ef44ce..b7d690d 100644
--- a/res-xxxhdpi/images/loop00048.png
+++ b/res-xxxhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00049.png b/res-xxxhdpi/images/loop00049.png
index 5c7b1c5..26ad6c2 100644
--- a/res-xxxhdpi/images/loop00049.png
+++ b/res-xxxhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00050.png b/res-xxxhdpi/images/loop00050.png
index 10dcf21..555d669 100644
--- a/res-xxxhdpi/images/loop00050.png
+++ b/res-xxxhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00051.png b/res-xxxhdpi/images/loop00051.png
index e850b32..f7defeb 100644
--- a/res-xxxhdpi/images/loop00051.png
+++ b/res-xxxhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00052.png b/res-xxxhdpi/images/loop00052.png
index 7abf444..915422d 100644
--- a/res-xxxhdpi/images/loop00052.png
+++ b/res-xxxhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00053.png b/res-xxxhdpi/images/loop00053.png
index f680849..2dd9b81 100644
--- a/res-xxxhdpi/images/loop00053.png
+++ b/res-xxxhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00054.png b/res-xxxhdpi/images/loop00054.png
index 012c14d..e5efd00 100644
--- a/res-xxxhdpi/images/loop00054.png
+++ b/res-xxxhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00055.png b/res-xxxhdpi/images/loop00055.png
index ae335db..aa9e379 100644
--- a/res-xxxhdpi/images/loop00055.png
+++ b/res-xxxhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00056.png b/res-xxxhdpi/images/loop00056.png
index 8e928ea..d5f355c 100644
--- a/res-xxxhdpi/images/loop00056.png
+++ b/res-xxxhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00057.png b/res-xxxhdpi/images/loop00057.png
index c23d4f0..1f06b37 100644
--- a/res-xxxhdpi/images/loop00057.png
+++ b/res-xxxhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00058.png b/res-xxxhdpi/images/loop00058.png
index d5144aa..34dc372 100644
--- a/res-xxxhdpi/images/loop00058.png
+++ b/res-xxxhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00059.png b/res-xxxhdpi/images/loop00059.png
index f8f3a7c..befec31 100644
--- a/res-xxxhdpi/images/loop00059.png
+++ b/res-xxxhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00060.png b/res-xxxhdpi/images/loop00060.png
index 8894a23..0bfb463 100644
--- a/res-xxxhdpi/images/loop00060.png
+++ b/res-xxxhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00061.png b/res-xxxhdpi/images/loop00061.png
index 1c33e84..ec622a9 100644
--- a/res-xxxhdpi/images/loop00061.png
+++ b/res-xxxhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00062.png b/res-xxxhdpi/images/loop00062.png
index c2242ff..5f0d50c 100644
--- a/res-xxxhdpi/images/loop00062.png
+++ b/res-xxxhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00063.png b/res-xxxhdpi/images/loop00063.png
index c357ffa..ce967bd 100644
--- a/res-xxxhdpi/images/loop00063.png
+++ b/res-xxxhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00064.png b/res-xxxhdpi/images/loop00064.png
index f946699..4b2f5d7 100644
--- a/res-xxxhdpi/images/loop00064.png
+++ b/res-xxxhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00065.png b/res-xxxhdpi/images/loop00065.png
index 52d976b..54ae196 100644
--- a/res-xxxhdpi/images/loop00065.png
+++ b/res-xxxhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00066.png b/res-xxxhdpi/images/loop00066.png
index cf37f2f..1c5a4d4 100644
--- a/res-xxxhdpi/images/loop00066.png
+++ b/res-xxxhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00067.png b/res-xxxhdpi/images/loop00067.png
index d8a1e78..7a016e7 100644
--- a/res-xxxhdpi/images/loop00067.png
+++ b/res-xxxhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00068.png b/res-xxxhdpi/images/loop00068.png
index 8bbaf02..39dbbda 100644
--- a/res-xxxhdpi/images/loop00068.png
+++ b/res-xxxhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00069.png b/res-xxxhdpi/images/loop00069.png
index 99d1072..ea2a1e9 100644
--- a/res-xxxhdpi/images/loop00069.png
+++ b/res-xxxhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00070.png b/res-xxxhdpi/images/loop00070.png
index bd8979e..303ee3f 100644
--- a/res-xxxhdpi/images/loop00070.png
+++ b/res-xxxhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00071.png b/res-xxxhdpi/images/loop00071.png
index e823dcc..74052b1 100644
--- a/res-xxxhdpi/images/loop00071.png
+++ b/res-xxxhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00072.png b/res-xxxhdpi/images/loop00072.png
index 475190f..8c2a549 100644
--- a/res-xxxhdpi/images/loop00072.png
+++ b/res-xxxhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00073.png b/res-xxxhdpi/images/loop00073.png
index 84c4874..6fdb8e5 100644
--- a/res-xxxhdpi/images/loop00073.png
+++ b/res-xxxhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00074.png b/res-xxxhdpi/images/loop00074.png
index e2d90a2..6afb236 100644
--- a/res-xxxhdpi/images/loop00074.png
+++ b/res-xxxhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00075.png b/res-xxxhdpi/images/loop00075.png
index ff13dfe..fc9aa77 100644
--- a/res-xxxhdpi/images/loop00075.png
+++ b/res-xxxhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00076.png b/res-xxxhdpi/images/loop00076.png
index 01886ae..5a49293 100644
--- a/res-xxxhdpi/images/loop00076.png
+++ b/res-xxxhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00077.png b/res-xxxhdpi/images/loop00077.png
index 4bac4ea..4739c13 100644
--- a/res-xxxhdpi/images/loop00077.png
+++ b/res-xxxhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00078.png b/res-xxxhdpi/images/loop00078.png
index 6ced1a2..9d79114 100644
--- a/res-xxxhdpi/images/loop00078.png
+++ b/res-xxxhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00079.png b/res-xxxhdpi/images/loop00079.png
index f7baed3..59be263 100644
--- a/res-xxxhdpi/images/loop00079.png
+++ b/res-xxxhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00080.png b/res-xxxhdpi/images/loop00080.png
index fbb0a13..7bcfbc9 100644
--- a/res-xxxhdpi/images/loop00080.png
+++ b/res-xxxhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00081.png b/res-xxxhdpi/images/loop00081.png
index 3fc7a49..3393f83 100644
--- a/res-xxxhdpi/images/loop00081.png
+++ b/res-xxxhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00082.png b/res-xxxhdpi/images/loop00082.png
index 3114002..f8d3013 100644
--- a/res-xxxhdpi/images/loop00082.png
+++ b/res-xxxhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00083.png b/res-xxxhdpi/images/loop00083.png
index df1b830..733db7b 100644
--- a/res-xxxhdpi/images/loop00083.png
+++ b/res-xxxhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00084.png b/res-xxxhdpi/images/loop00084.png
index 11a72f2..d71a10c 100644
--- a/res-xxxhdpi/images/loop00084.png
+++ b/res-xxxhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00085.png b/res-xxxhdpi/images/loop00085.png
index ba0a43e..0ea2a19 100644
--- a/res-xxxhdpi/images/loop00085.png
+++ b/res-xxxhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00086.png b/res-xxxhdpi/images/loop00086.png
index c4111b2..d2ea05e 100644
--- a/res-xxxhdpi/images/loop00086.png
+++ b/res-xxxhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00087.png b/res-xxxhdpi/images/loop00087.png
index 13b83c7..f7814f0 100644
--- a/res-xxxhdpi/images/loop00087.png
+++ b/res-xxxhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00088.png b/res-xxxhdpi/images/loop00088.png
index e7d9d6d..b82ab4c 100644
--- a/res-xxxhdpi/images/loop00088.png
+++ b/res-xxxhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00089.png b/res-xxxhdpi/images/loop00089.png
index fd1951c..ed354e6 100644
--- a/res-xxxhdpi/images/loop00089.png
+++ b/res-xxxhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00090.png b/res-xxxhdpi/images/loop00090.png
index d6640c5..3bca599 100644
--- a/res-xxxhdpi/images/loop00090.png
+++ b/res-xxxhdpi/images/loop00090.png
Binary files differ
diff --git a/roots.cpp b/roots.cpp
index 184e799..c29771a 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -39,7 +39,7 @@
 #include <ext4_utils/wipe.h>
 #include <fs_mgr.h>
 
-#include "mounts.h"
+#include "otautil/mounts.h"
 
 static struct fstab* fstab = nullptr;
 
@@ -126,7 +126,7 @@
   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) {
+      strcmp(v->fs_type, "vfat") == 0 || strcmp(v->fs_type, "f2fs") == 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";
@@ -283,7 +283,7 @@
   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),
+      "/system/bin/mke2fs", "-F", "-t", "ext4", "-b", std::to_string(kBlockSize),
     };
 
     int raid_stride = v->logical_blk_size / kBlockSize;
@@ -305,13 +305,7 @@
     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,
+        "/system/bin/e2fsdroid", "-e", "-f", directory, "-a", volume, v->blk_device,
       };
       result = exec_cmd(e2fsdroid_args);
     }
diff --git a/rotate_logs.cpp b/rotate_logs.cpp
deleted file mode 100644
index da00879..0000000
--- a/rotate_logs.cpp
+++ /dev/null
@@ -1,107 +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.
- */
-
-#include "rotate_logs.h"
-
-#include <stdio.h>
-#include <string.h>
-#include <sys/types.h>
-
-#include <string>
-
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/parseint.h>
-#include <android-base/stringprintf.h>
-#include <private/android_logger.h> /* private pmsg functions */
-
-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 /* 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 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);
-
-  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";
-  } 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);
-    }
-  }
-
-  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;
-
-  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/screen_ui.cpp b/screen_ui.cpp
index c8fb5aa..391dedb 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -19,8 +19,6 @@
 #include <dirent.h>
 #include <errno.h>
 #include <fcntl.h>
-#include <linux/input.h>
-#include <pthread.h>
 #include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
@@ -31,8 +29,11 @@
 #include <time.h>
 #include <unistd.h>
 
+#include <algorithm>
+#include <chrono>
 #include <memory>
 #include <string>
+#include <thread>
 #include <unordered_map>
 #include <vector>
 
@@ -40,10 +41,10 @@
 #include <android-base/properties.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 "otautil/paths.h"
 #include "ui.h"
 
 // Return the current time as a double (including fractions of a second).
@@ -53,11 +54,106 @@
   return tv.tv_sec + tv.tv_usec / 1000000.0;
 }
 
-ScreenRecoveryUI::ScreenRecoveryUI()
-    : kMarginWidth(RECOVERY_UI_MARGIN_WIDTH),
-      kMarginHeight(RECOVERY_UI_MARGIN_HEIGHT),
-      kAnimationFps(RECOVERY_UI_ANIMATION_FPS),
-      kDensity(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
+Menu::Menu(bool scrollable, size_t max_items, size_t max_length,
+           const std::vector<std::string>& headers, const std::vector<std::string>& items,
+           size_t initial_selection)
+    : scrollable_(scrollable),
+      max_display_items_(max_items),
+      max_item_length_(max_length),
+      text_headers_(headers),
+      menu_start_(0),
+      selection_(initial_selection) {
+  CHECK_LE(max_items, static_cast<size_t>(std::numeric_limits<int>::max()));
+
+  // It's fine to have more entries than text_rows_ if scrollable menu is supported.
+  size_t items_count = scrollable_ ? items.size() : std::min(items.size(), max_display_items_);
+  for (size_t i = 0; i < items_count; ++i) {
+    text_items_.emplace_back(items[i].substr(0, max_item_length_));
+  }
+
+  CHECK(!text_items_.empty());
+}
+
+const std::vector<std::string>& Menu::text_headers() const {
+  return text_headers_;
+}
+
+std::string Menu::TextItem(size_t index) const {
+  CHECK_LT(index, text_items_.size());
+
+  return text_items_[index];
+}
+
+size_t Menu::MenuStart() const {
+  return menu_start_;
+}
+
+size_t Menu::MenuEnd() const {
+  return std::min(ItemsCount(), menu_start_ + max_display_items_);
+}
+
+size_t Menu::ItemsCount() const {
+  return text_items_.size();
+}
+
+bool Menu::ItemsOverflow(std::string* cur_selection_str) const {
+  if (!scrollable_ || ItemsCount() <= max_display_items_) {
+    return false;
+  }
+
+  *cur_selection_str =
+      android::base::StringPrintf("Current item: %zu/%zu", selection_ + 1, ItemsCount());
+  return true;
+}
+
+// TODO(xunchang) modify the function parameters to button up & down.
+int Menu::Select(int sel) {
+  CHECK_LE(ItemsCount(), static_cast<size_t>(std::numeric_limits<int>::max()));
+  int count = ItemsCount();
+
+  // Wraps the selection at boundary if the menu is not scrollable.
+  if (!scrollable_) {
+    if (sel < 0) {
+      selection_ = count - 1;
+    } else if (sel >= count) {
+      selection_ = 0;
+    } else {
+      selection_ = sel;
+    }
+
+    return selection_;
+  }
+
+  if (sel < 0) {
+    selection_ = 0;
+  } else if (sel >= count) {
+    selection_ = count - 1;
+  } else {
+    if (static_cast<size_t>(sel) < menu_start_) {
+      menu_start_--;
+    } else if (static_cast<size_t>(sel) >= MenuEnd()) {
+      menu_start_++;
+    }
+    selection_ = sel;
+  }
+
+  return selection_;
+}
+
+ScreenRecoveryUI::ScreenRecoveryUI() : ScreenRecoveryUI(false) {}
+
+constexpr int kDefaultMarginHeight = 0;
+constexpr int kDefaultMarginWidth = 0;
+constexpr int kDefaultAnimationFps = 30;
+
+ScreenRecoveryUI::ScreenRecoveryUI(bool scrollable_menu)
+    : margin_width_(
+          android::base::GetIntProperty("ro.recovery.ui.margin_width", kDefaultMarginWidth)),
+      margin_height_(
+          android::base::GetIntProperty("ro.recovery.ui.margin_height", kDefaultMarginHeight)),
+      animation_fps_(
+          android::base::GetIntProperty("ro.recovery.ui.animation_fps", kDefaultAnimationFps)),
+      density_(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
       currentIcon(NONE),
       progressBarType(EMPTY),
       progressScopeStart(0),
@@ -71,10 +167,7 @@
       text_row_(0),
       show_text(false),
       show_text_ever(false),
-      menu_headers_(nullptr),
-      show_menu(false),
-      menu_items(0),
-      menu_sel(0),
+      scrollable_menu_(scrollable_menu),
       file_viewer_text_(nullptr),
       intro_frames(0),
       loop_frames(0),
@@ -83,8 +176,16 @@
       stage(-1),
       max_stage(-1),
       locale_(""),
-      rtl_locale_(false),
-      updateMutex(PTHREAD_MUTEX_INITIALIZER) {}
+      rtl_locale_(false) {}
+
+ScreenRecoveryUI::~ScreenRecoveryUI() {
+  progress_thread_stopped_ = true;
+  if (progress_thread_.joinable()) {
+    progress_thread_.join();
+  }
+  // No-op if gr_init() (via Init()) was not called or had failed.
+  gr_exit();
+}
 
 GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
   if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
@@ -109,7 +210,7 @@
 }
 
 int ScreenRecoveryUI::PixelsFromDp(int dp) const {
-  return dp * kDensity;
+  return dp * density_;
 }
 
 // Here's the intended layout:
@@ -164,7 +265,7 @@
       int stage_height = gr_get_height(stageMarkerEmpty);
       int stage_width = gr_get_width(stageMarkerEmpty);
       int x = (ScreenWidth() - max_stage * gr_get_width(stageMarkerEmpty)) / 2;
-      int y = ScreenHeight() - stage_height - kMarginHeight;
+      int y = ScreenHeight() - stage_height - margin_height_;
       for (int i = 0; i < max_stage; ++i) {
         GRSurface* stage_surface = (i < stage) ? stageMarkerFill : stageMarkerEmpty;
         DrawSurface(stage_surface, 0, 0, stage_width, stage_height, x, y);
@@ -275,51 +376,55 @@
     surfaces.emplace(name, std::unique_ptr<GRSurface, decltype(&free)>(text_image, &free));
   }
 
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   gr_color(0, 0, 0, 255);
   gr_clear();
 
-  int text_y = kMarginHeight;
-  int text_x = kMarginWidth;
+  int text_y = margin_height_;
+  int text_x = margin_width_;
   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);
+  text_y += DrawTextLine(text_x, text_y, header, 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 };
+      "Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel + 1, locales_entries.size());
+  // clang-format off
+  std::vector<std::string> instruction = {
+    locale_selection,
+    "Use volume up/down to switch locales and power to exit."
+  };
+  // clang-format on
   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);
+    text_y += DrawTextLine(text_x, text_y, p.first, 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) {
+void ScreenRecoveryUI::CheckBackgroundTextImages() {
   // 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;
   }
+  std::string saved_locale = locale_;
   size_t selected = 0;
   SelectAndShowBackgroundText(locales_entries, selected);
 
   FlushKeys();
   while (true) {
     int key = WaitKey();
+    if (key == static_cast<int>(KeyError::INTERRUPTED)) break;
     if (key == KEY_POWER || key == KEY_ENTER) {
       break;
     } else if (key == KEY_UP || key == KEY_VOLUMEUP) {
@@ -364,57 +469,51 @@
   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);
+int ScreenRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
+  gr_text(gr_sys_font(), x, y, line.c_str(), bold);
   return char_height_ + 4;
 }
 
-int ScreenRecoveryUI::DrawTextLines(int x, int y, const char* const* lines) const {
+int ScreenRecoveryUI::DrawTextLines(int x, int y, const std::vector<std::string>& lines) const {
   int offset = 0;
-  for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
-    offset += DrawTextLine(x, y + offset, lines[i], false);
+  for (const auto& line : lines) {
+    offset += DrawTextLine(x, y + offset, line, false);
   }
   return offset;
 }
 
-int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const char* const* lines) const {
+int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y,
+                                           const std::vector<std::string>& lines) const {
+  // Keep symmetrical margins based on the given offset (i.e. x).
+  size_t text_cols = (ScreenWidth() - x * 2) / char_width_;
   int offset = 0;
-  for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
-    // The line will be wrapped if it exceeds text_cols_.
-    std::string line(lines[i]);
+  for (const auto& line : lines) {
     size_t next_start = 0;
     while (next_start < line.size()) {
-      std::string sub = line.substr(next_start, text_cols_ + 1);
-      if (sub.size() <= text_cols_) {
+      std::string sub = line.substr(next_start, text_cols + 1);
+      if (sub.size() <= text_cols) {
         next_start += sub.size();
       } else {
-        // Line too long and must be wrapped to text_cols_ columns.
+        // Line too long and must be wrapped to text_cols columns.
         size_t last_space = sub.find_last_of(" \t\n");
         if (last_space == std::string::npos) {
-          // No space found, just draw as much as we can
-          sub.resize(text_cols_);
-          next_start += text_cols_;
+          // No space found, just draw as much as we can.
+          sub.resize(text_cols);
+          next_start += text_cols;
         } else {
           sub.resize(last_space);
           next_start += last_space + 1;
         }
       }
-      offset += DrawTextLine(x, y + offset, sub.c_str(), false);
+      offset += DrawTextLine(x, y + offset, sub, false);
     }
   }
   return offset;
 }
 
-static const char* REGULAR_HELP[] = {
-  "Use volume up/down and power.",
-  NULL
-};
-
-static const char* LONG_PRESS_HELP[] = {
-  "Any button cycles highlight.",
-  "Long-press activates.",
-  NULL
-};
+void ScreenRecoveryUI::SetTitle(const std::vector<std::string>& lines) {
+  title_lines_ = lines;
+}
 
 // Redraws everything on the screen. Does not flip pages. Should only be called with updateMutex
 // locked.
@@ -428,38 +527,71 @@
   gr_color(0, 0, 0, 255);
   gr_clear();
 
-  int y = kMarginHeight;
-  if (show_menu) {
+  // clang-format off
+  static std::vector<std::string> REGULAR_HELP{
+    "Use volume up/down and power.",
+  };
+  static std::vector<std::string> LONG_PRESS_HELP{
+    "Any button cycles highlight.",
+    "Long-press activates.",
+  };
+  // clang-format on
+  draw_menu_and_text_buffer_locked(HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
+}
+
+// Draws the menu and text buffer on the screen. Should only be called with updateMutex locked.
+void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(
+    const std::vector<std::string>& help_message) {
+  int y = margin_height_;
+  if (menu_) {
     static constexpr int kMenuIndent = 4;
-    int x = kMarginWidth + kMenuIndent;
+    int x = margin_width_ + kMenuIndent;
 
     SetColor(INFO);
-    y += DrawTextLine(x, y, "Android Recovery", true);
-    std::string recovery_fingerprint =
-        android::base::GetProperty("ro.bootimage.build.fingerprint", "");
-    for (const auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
-      y += DrawTextLine(x, y, chunk.c_str(), false);
+
+    for (size_t i = 0; i < title_lines_.size(); i++) {
+      y += DrawTextLine(x, y, title_lines_[i], i == 0);
     }
-    y += DrawTextLines(x, y, HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
 
+    y += DrawTextLines(x, y, help_message);
+
+    // Draw menu header.
     SetColor(HEADER);
-    // Ignore kMenuIndent, which is not taken into account by text_cols_.
-    y += DrawWrappedTextLines(kMarginWidth, y, menu_headers_);
+    if (!menu_->scrollable()) {
+      y += DrawWrappedTextLines(x, y, menu_->text_headers());
+    } else {
+      y += DrawTextLines(x, y, menu_->text_headers());
+      // Show the current menu item number in relation to total number if items don't fit on the
+      // screen.
+      std::string cur_selection_str;
+      if (menu_->ItemsOverflow(&cur_selection_str)) {
+        y += DrawTextLine(x, y, cur_selection_str, true);
+      }
+    }
 
+    // Draw menu items.
     SetColor(MENU);
-    y += DrawHorizontalRule(y) + 4;
-    for (int i = 0; i < menu_items; ++i) {
-      if (i == menu_sel) {
+    // Do not draw the horizontal rule for wear devices.
+    if (!menu_->scrollable()) {
+      y += DrawHorizontalRule(y) + 4;
+    }
+    for (size_t i = menu_->MenuStart(); i < menu_->MenuEnd(); ++i) {
+      bool bold = false;
+      if (i == static_cast<size_t>(menu_->selection())) {
         // Draw the highlight bar.
         SetColor(IsLongPress() ? MENU_SEL_BG_ACTIVE : MENU_SEL_BG);
-        DrawHighlightBar(0, y - 2, ScreenWidth(), char_height_ + 4);
+
+        int bar_height = char_height_ + 4;
+        DrawHighlightBar(0, y - 2, ScreenWidth(), bar_height);
+
         // Bold white text for the selected item.
         SetColor(MENU_SEL_FG);
-        y += DrawTextLine(x, y, menu_[i].c_str(), true);
-        SetColor(MENU);
-      } else {
-        y += DrawTextLine(x, y, menu_[i].c_str(), false);
+        bold = true;
       }
+
+      y += DrawTextLine(x, y, menu_->TextItem(i), bold);
+
+      SetColor(MENU);
     }
     y += DrawHorizontalRule(y);
   }
@@ -469,9 +601,9 @@
   SetColor(LOG);
   int row = text_row_;
   size_t count = 0;
-  for (int ty = ScreenHeight() - kMarginHeight - char_height_; ty >= y && count < text_rows_;
+  for (int ty = ScreenHeight() - margin_height_ - char_height_; ty >= y && count < text_rows_;
        ty -= char_height_, ++count) {
-    DrawTextLine(kMarginWidth, ty, text_[row], false);
+    DrawTextLine(margin_width_, ty, text_[row], false);
     --row;
     if (row < 0) row = text_rows_ - 1;
   }
@@ -496,52 +628,46 @@
   gr_flip();
 }
 
-// Keeps the progress bar updated, even when the process is otherwise busy.
-void* ScreenRecoveryUI::ProgressThreadStartRoutine(void* data) {
-  reinterpret_cast<ScreenRecoveryUI*>(data)->ProgressThreadLoop();
-  return nullptr;
-}
-
 void ScreenRecoveryUI::ProgressThreadLoop() {
-  double interval = 1.0 / kAnimationFps;
-  while (true) {
+  double interval = 1.0 / animation_fps_;
+  while (!progress_thread_stopped_) {
     double start = now();
-    pthread_mutex_lock(&updateMutex);
-
     bool redraw = false;
+    {
+      std::lock_guard<std::mutex> lg(updateMutex);
 
-    // update the installation animation, if active
-    // skip this if we have a text overlay (too expensive to update)
-    if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) && !show_text) {
-      if (!intro_done) {
-        if (current_frame == intro_frames - 1) {
-          intro_done = true;
-          current_frame = 0;
+      // update the installation animation, if active
+      // skip this if we have a text overlay (too expensive to update)
+      if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) && !show_text) {
+        if (!intro_done) {
+          if (current_frame == intro_frames - 1) {
+            intro_done = true;
+            current_frame = 0;
+          } else {
+            ++current_frame;
+          }
         } else {
-          ++current_frame;
+          current_frame = (current_frame + 1) % loop_frames;
         }
-      } else {
-        current_frame = (current_frame + 1) % loop_frames;
-      }
 
-      redraw = true;
-    }
-
-    // move the progress bar forward on timed intervals, if configured
-    int duration = progressScopeDuration;
-    if (progressBarType == DETERMINATE && duration > 0) {
-      double elapsed = now() - progressScopeTime;
-      float p = 1.0 * elapsed / duration;
-      if (p > 1.0) p = 1.0;
-      if (p > progress) {
-        progress = p;
         redraw = true;
       }
+
+      // move the progress bar forward on timed intervals, if configured
+      int duration = progressScopeDuration;
+      if (progressBarType == DETERMINATE && duration > 0) {
+        double elapsed = now() - progressScopeTime;
+        float p = 1.0 * elapsed / duration;
+        if (p > 1.0) p = 1.0;
+        if (p > progress) {
+          progress = p;
+          redraw = true;
+        }
+      }
+
+      if (redraw) update_progress_locked();
     }
 
-    if (redraw) update_progress_locked();
-
-    pthread_mutex_unlock(&updateMutex);
     double end = now();
     // minimum of 20ms delay between frames
     double delay = interval - (end - start);
@@ -584,19 +710,23 @@
 }
 
 bool ScreenRecoveryUI::InitTextParams() {
-  if (gr_init() < 0) {
+  // gr_init() would return successfully on font initialization failure.
+  if (gr_sys_font() == nullptr) {
     return false;
   }
-
   gr_font_size(gr_sys_font(), &char_width_, &char_height_);
-  text_rows_ = (ScreenHeight() - kMarginHeight * 2) / char_height_;
-  text_cols_ = (ScreenWidth() - kMarginWidth * 2) / char_width_;
+  text_rows_ = (ScreenHeight() - margin_height_ * 2) / char_height_;
+  text_cols_ = (ScreenWidth() - margin_width_ * 2) / char_width_;
   return true;
 }
 
 bool ScreenRecoveryUI::Init(const std::string& locale) {
   RecoveryUI::Init(locale);
 
+  if (gr_init() == -1) {
+    return false;
+  }
+
   if (!InitTextParams()) {
     return false;
   }
@@ -632,13 +762,19 @@
 
   LoadAnimation();
 
-  pthread_create(&progress_thread_, nullptr, ProgressThreadStartRoutine, this);
+  // Keep the progress bar updated, even when the process is otherwise busy.
+  progress_thread_ = std::thread(&ScreenRecoveryUI::ProgressThreadLoop, this);
 
   return true;
 }
 
+std::string ScreenRecoveryUI::GetLocale() const {
+  return locale_;
+}
+
 void ScreenRecoveryUI::LoadAnimation() {
-  std::unique_ptr<DIR, decltype(&closedir)> dir(opendir("/res/images"), closedir);
+  std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(Paths::Get().resource_dir().c_str()),
+                                                closedir);
   dirent* de;
   std::vector<std::string> intro_frame_names;
   std::vector<std::string> loop_frame_names;
@@ -675,16 +811,14 @@
 }
 
 void ScreenRecoveryUI::SetBackground(Icon icon) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
 
   currentIcon = icon;
   update_screen_locked();
-
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::SetProgressType(ProgressType type) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   if (progressBarType != type) {
     progressBarType = type;
   }
@@ -692,11 +826,10 @@
   progressScopeSize = 0;
   progress = 0;
   update_progress_locked();
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::ShowProgress(float portion, float seconds) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   progressBarType = DETERMINATE;
   progressScopeStart += progressScopeSize;
   progressScopeSize = portion;
@@ -704,11 +837,10 @@
   progressScopeDuration = seconds;
   progress = 0;
   update_progress_locked();
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::SetProgress(float fraction) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   if (fraction < 0.0) fraction = 0.0;
   if (fraction > 1.0) fraction = 1.0;
   if (progressBarType == DETERMINATE && fraction > progress) {
@@ -720,14 +852,12 @@
       update_progress_locked();
     }
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::SetStage(int current, int max) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   stage = current;
   max_stage = max;
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::PrintV(const char* fmt, bool copy_to_stdout, va_list ap) {
@@ -738,7 +868,7 @@
     fputs(str.c_str(), stdout);
   }
 
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(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_) {
@@ -751,7 +881,6 @@
     text_[text_row_][text_col_] = '\0';
     update_screen_locked();
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::Print(const char* fmt, ...) {
@@ -769,23 +898,21 @@
 }
 
 void ScreenRecoveryUI::PutChar(char ch) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(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 ScreenRecoveryUI::ClearText() {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   text_col_ = 0;
   text_row_ = 0;
   for (size_t i = 0; i < text_rows_; ++i) {
     memset(text_[i], 0, text_cols_ + 1);
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::ShowFile(FILE* fp) {
@@ -806,6 +933,7 @@
       while (show_prompt) {
         show_prompt = false;
         int key = WaitKey();
+        if (key == static_cast<int>(KeyError::INTERRUPTED)) return;
         if (key == KEY_POWER || key == KEY_ENTER) {
           return;
         } else if (key == KEY_UP || key == KEY_VOLUMEUP) {
@@ -838,10 +966,10 @@
   }
 }
 
-void ScreenRecoveryUI::ShowFile(const char* filename) {
-  FILE* fp = fopen_path(filename, "re");
-  if (fp == nullptr) {
-    Print("  Unable to open %s: %s\n", filename, strerror(errno));
+void ScreenRecoveryUI::ShowFile(const std::string& filename) {
+  std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(filename.c_str(), "re"), fclose);
+  if (!fp) {
+    Print("  Unable to open %s: %s\n", filename.c_str(), strerror(errno));
     return;
   }
 
@@ -853,83 +981,121 @@
   text_ = file_viewer_text_;
   ClearText();
 
-  ShowFile(fp);
-  fclose(fp);
+  ShowFile(fp.get());
 
   text_ = old_text;
   text_col_ = old_text_col;
   text_row_ = old_text_row;
 }
 
-void ScreenRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
-                                 int initial_selection) {
-  pthread_mutex_lock(&updateMutex);
-  if (text_rows_ > 0 && text_cols_ > 0) {
-    menu_headers_ = headers;
-    menu_.clear();
-    for (size_t i = 0; i < text_rows_ && items[i] != nullptr; ++i) {
-      menu_.emplace_back(std::string(items[i], strnlen(items[i], text_cols_ - 1)));
-    }
-    menu_items = static_cast<int>(menu_.size());
-    show_menu = true;
-    menu_sel = initial_selection;
+void ScreenRecoveryUI::StartMenu(const std::vector<std::string>& headers,
+                                 const std::vector<std::string>& items, size_t initial_selection) {
+  std::lock_guard<std::mutex> lg(updateMutex);
+  if (text_rows_ > 0 && text_cols_ > 1) {
+    menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
+                                   initial_selection);
     update_screen_locked();
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 int ScreenRecoveryUI::SelectMenu(int sel) {
-  pthread_mutex_lock(&updateMutex);
-  if (show_menu) {
-    int old_sel = menu_sel;
-    menu_sel = sel;
+  std::lock_guard<std::mutex> lg(updateMutex);
+  if (menu_) {
+    int old_sel = menu_->selection();
+    sel = menu_->Select(sel);
 
-    // Wrap at top and bottom.
-    if (menu_sel < 0) menu_sel = menu_items - 1;
-    if (menu_sel >= menu_items) menu_sel = 0;
-
-    sel = menu_sel;
-    if (menu_sel != old_sel) update_screen_locked();
+    if (sel != old_sel) {
+      update_screen_locked();
+    }
   }
-  pthread_mutex_unlock(&updateMutex);
   return sel;
 }
 
 void ScreenRecoveryUI::EndMenu() {
-  pthread_mutex_lock(&updateMutex);
-  if (show_menu && text_rows_ > 0 && text_cols_ > 0) {
-    show_menu = false;
+  std::lock_guard<std::mutex> lg(updateMutex);
+  if (menu_) {
+    menu_.reset();
     update_screen_locked();
   }
-  pthread_mutex_unlock(&updateMutex);
+}
+
+size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers,
+                                  const std::vector<std::string>& items, size_t initial_selection,
+                                  bool menu_only,
+                                  const std::function<int(int, bool)>& key_handler) {
+  // Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
+  FlushKeys();
+
+  // If there is a key interrupt in progress, return KeyError::INTERRUPTED without starting the
+  // menu.
+  if (IsKeyInterrupted()) return static_cast<size_t>(KeyError::INTERRUPTED);
+
+  StartMenu(headers, items, initial_selection);
+
+  int selected = initial_selection;
+  int chosen_item = -1;
+  while (chosen_item < 0) {
+    int key = WaitKey();
+    if (key == static_cast<int>(KeyError::INTERRUPTED)) {  // WaitKey() was interrupted.
+      return static_cast<size_t>(KeyError::INTERRUPTED);
+    }
+    if (key == static_cast<int>(KeyError::TIMED_OUT)) {  // WaitKey() timed out.
+      if (WasTextEverVisible()) {
+        continue;
+      } else {
+        LOG(INFO) << "Timed out waiting for key input; rebooting.";
+        EndMenu();
+        return static_cast<size_t>(KeyError::TIMED_OUT);
+      }
+    }
+
+    bool visible = IsTextVisible();
+    int action = key_handler(key, visible);
+    if (action < 0) {
+      switch (action) {
+        case Device::kHighlightUp:
+          selected = SelectMenu(--selected);
+          break;
+        case Device::kHighlightDown:
+          selected = SelectMenu(++selected);
+          break;
+        case Device::kInvokeItem:
+          chosen_item = selected;
+          break;
+        case Device::kNoAction:
+          break;
+      }
+    } else if (!menu_only) {
+      chosen_item = action;
+    }
+  }
+
+  EndMenu();
+  return chosen_item;
 }
 
 bool ScreenRecoveryUI::IsTextVisible() {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   int visible = show_text;
-  pthread_mutex_unlock(&updateMutex);
   return visible;
 }
 
 bool ScreenRecoveryUI::WasTextEverVisible() {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   int ever_visible = show_text_ever;
-  pthread_mutex_unlock(&updateMutex);
   return ever_visible;
 }
 
 void ScreenRecoveryUI::ShowText(bool visible) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   show_text = visible;
   if (show_text) show_text_ever = true;
   update_screen_locked();
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::Redraw() {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
   update_screen_locked();
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::KeyLongPress(int) {
@@ -943,9 +1109,9 @@
   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);
+    size_t separator = new_locale.find('-');
+    // lang has the language prefix prior to the separator, or full string if none exists.
+    std::string lang = new_locale.substr(0, separator);
 
     // A bit cheesy: keep an explicit list of supported RTL languages.
     if (lang == "ar" ||  // Arabic
diff --git a/screen_ui.h b/screen_ui.h
index f05761c..f08f4f4 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -17,10 +17,13 @@
 #ifndef RECOVERY_SCREEN_UI_H
 #define RECOVERY_SCREEN_UI_H
 
-#include <pthread.h>
 #include <stdio.h>
 
+#include <atomic>
+#include <functional>
+#include <memory>
 #include <string>
+#include <thread>
 #include <vector>
 
 #include "ui.h"
@@ -28,6 +31,71 @@
 // From minui/minui.h.
 struct GRSurface;
 
+// This class maintains the menu selection and display of the screen ui.
+class Menu {
+ public:
+  // Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial
+  // selection to |initial_selection|.
+  Menu(bool scrollable, size_t max_items, size_t max_length,
+       const std::vector<std::string>& headers, const std::vector<std::string>& items,
+       size_t initial_selection);
+
+  bool scrollable() const {
+    return scrollable_;
+  }
+
+  size_t selection() const {
+    return selection_;
+  }
+
+  // Returns count of menu items.
+  size_t ItemsCount() const;
+
+  // Returns the index of the first menu item.
+  size_t MenuStart() const;
+
+  // Returns the index of the last menu item + 1.
+  size_t MenuEnd() const;
+
+  // Menu example:
+  // info:                           Android Recovery
+  //                                 ....
+  // help messages:                  Swipe up/down to move
+  //                                 Swipe left/right to select
+  // empty line (horizontal rule):
+  // menu headers:                   Select file to view
+  // menu items:                     /cache/recovery/last_log
+  //                                 /cache/recovery/last_log.1
+  //                                 /cache/recovery/last_log.2
+  //                                 ...
+  const std::vector<std::string>& text_headers() const;
+  std::string TextItem(size_t index) const;
+
+  // Checks if the menu items fit vertically on the screen. Returns true and set the
+  // |cur_selection_str| if the items exceed the screen limit.
+  bool ItemsOverflow(std::string* cur_selection_str) const;
+
+  // Sets the current selection to |sel|. Handle the overflow cases depending on if the menu is
+  // scrollable.
+  int Select(int sel);
+
+ private:
+  // The menu is scrollable to display more items. Used on wear devices who have smaller screens.
+  const bool scrollable_;
+  // The max number of menu items to fit vertically on a screen.
+  const size_t max_display_items_;
+  // The length of each item to fit horizontally on a screen.
+  const size_t max_item_length_;
+  // The menu headers.
+  std::vector<std::string> text_headers_;
+  // The actual menu items trimmed to fit the given properties.
+  std::vector<std::string> text_items_;
+  // The first item to display on the screen.
+  size_t menu_start_;
+  // Current menu selection.
+  size_t selection_;
+};
+
 // Implementation of RecoveryUI appropriate for devices with a screen
 // (shows an icon + a progress bar, text logging, menu, etc.)
 class ScreenRecoveryUI : public RecoveryUI {
@@ -44,8 +112,11 @@
   };
 
   ScreenRecoveryUI();
+  explicit ScreenRecoveryUI(bool scrollable_menu);
+  ~ScreenRecoveryUI() override;
 
   bool Init(const std::string& locale) override;
+  std::string GetLocale() const override;
 
   // overall recovery state ("background image")
   void SetBackground(Icon icon) override;
@@ -66,13 +137,13 @@
   // printing messages
   void Print(const char* fmt, ...) override __printflike(2, 3);
   void PrintOnScreenOnly(const char* fmt, ...) override __printflike(2, 3);
-  void ShowFile(const char* filename) override;
+  void ShowFile(const std::string& filename) override;
 
   // menu display
-  void StartMenu(const char* const* headers, const char* const* items,
-                 int initial_selection) override;
-  int SelectMenu(int sel) override;
-  void EndMenu() override;
+  size_t ShowMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
+                  size_t initial_selection, bool menu_only,
+                  const std::function<int(int, bool)>& key_handler) override;
+  void SetTitle(const std::vector<std::string>& lines) override;
 
   void KeyLongPress(int) override;
 
@@ -80,34 +151,46 @@
 
   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);
+  // Checks the background text image, for debugging purpose. It iterates the locales embedded in
+  // the on-device resource files and shows the localized text, for manual inspection.
+  void CheckBackgroundTextImages();
 
  protected:
   // The margin that we don't want to use for showing texts (e.g. round screen, or screen with
   // rounded corners).
-  const int kMarginWidth;
-  const int kMarginHeight;
+  const int margin_width_;
+  const int margin_height_;
 
   // Number of frames per sec (default: 30) for both parts of the animation.
-  const int kAnimationFps;
+  const int animation_fps_;
 
   // The scale factor from dp to pixels. 1.0 for mdpi, 4.0 for xxxhdpi.
-  const float kDensity;
+  const float density_;
 
   virtual bool InitTextParams();
 
+  // Displays some header text followed by a menu of items, which appears at the top of the screen
+  // (in place of any scrolling ui_print() output, if necessary).
+  virtual void StartMenu(const std::vector<std::string>& headers,
+                         const std::vector<std::string>& items, size_t initial_selection);
+
+  // Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
+  // selected.
+  virtual int SelectMenu(int sel);
+
+  // Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed.
+  virtual void EndMenu();
+
   virtual void draw_background_locked();
   virtual void draw_foreground_locked();
   virtual void draw_screen_locked();
+  virtual void draw_menu_and_text_buffer_locked(const std::vector<std::string>& help_message);
   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*);
@@ -134,7 +217,7 @@
   // 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;
+  virtual int DrawTextLine(int x, int y, const std::string& 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).
@@ -142,10 +225,11 @@
   // 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;
+  int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const;
+  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It
+  // keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving
+  // along Y-axis.
+  int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const;
 
   Icon currentIcon;
 
@@ -184,15 +268,16 @@
   bool show_text;
   bool show_text_ever;  // has show_text ever been true?
 
-  std::vector<std::string> menu_;
-  const char* const* menu_headers_;
-  bool show_menu;
-  int menu_items, menu_sel;
+  std::vector<std::string> title_lines_;
+
+  bool scrollable_menu_;
+  std::unique_ptr<Menu> menu_;
 
   // An alternate text screen, swapped with 'text_' when we're viewing a log file.
   char** file_viewer_text_;
 
-  pthread_t progress_thread_;
+  std::thread progress_thread_;
+  std::atomic<bool> progress_thread_stopped_{ false };
 
   // Number of intro frames and loop frames in the animation.
   size_t intro_frames;
@@ -210,7 +295,7 @@
   std::string locale_;
   bool rtl_locale_;
 
-  pthread_mutex_t updateMutex;
+  std::mutex updateMutex;
 
  private:
   void SetLocale(const std::string&);
diff --git a/stub_ui.h b/stub_ui.h
index 1f6b29a..a3cf12b 100644
--- a/stub_ui.h
+++ b/stub_ui.h
@@ -17,6 +17,10 @@
 #ifndef RECOVERY_STUB_UI_H
 #define RECOVERY_STUB_UI_H
 
+#include <functional>
+#include <string>
+#include <vector>
+
 #include "ui.h"
 
 // Stub implementation of RecoveryUI for devices without screen.
@@ -24,6 +28,9 @@
  public:
   StubRecoveryUI() = default;
 
+  std::string GetLocale() const override {
+    return "";
+  }
   void SetBackground(Icon /* icon */) override {}
   void SetSystemUpdateText(bool /* security_update */) override {}
 
@@ -51,15 +58,17 @@
     va_end(ap);
   }
   void PrintOnScreenOnly(const char* /* fmt */, ...) override {}
-  void ShowFile(const char* /* filename */) override {}
+  void ShowFile(const std::string& /* filename */) override {}
 
   // menu display
-  void StartMenu(const char* const* /* headers */, const char* const* /* items */,
-                 int /* initial_selection */) override {}
-  int SelectMenu(int sel) override {
-    return sel;
+  size_t ShowMenu(const std::vector<std::string>& /* headers */,
+                  const std::vector<std::string>& /* items */, size_t initial_selection,
+                  bool /* menu_only */,
+                  const std::function<int(int, bool)>& /* key_handler */) override {
+    return initial_selection;
   }
-  void EndMenu() override {}
+
+  void SetTitle(const std::vector<std::string>& /* lines */) override {}
 };
 
 #endif  // RECOVERY_STUB_UI_H
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..2cfc325
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,205 @@
+// 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.
+
+cc_defaults {
+    name: "recovery_test_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    include_dirs: [
+        "bootable/recovery",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+        "libcutils",
+        "liblog",
+        "libpng",
+        "libselinux",
+        "libz",
+        "libziparchive",
+    ],
+
+    target: {
+        android: {
+            shared_libs: [
+                "libutils",
+            ],
+        },
+
+        host: {
+            static_libs: [
+                "libutils",
+            ],
+        },
+    },
+}
+
+// libapplypatch, libapplypatch_modes, libimgdiff, libimgpatch
+libapplypatch_static_libs = [
+    "libapplypatch_modes",
+    "libapplypatch",
+    "libedify",
+    "libimgdiff",
+    "libimgpatch",
+    "libotautil",
+    "libbsdiff",
+    "libbspatch",
+    "libdivsufsort",
+    "libdivsufsort64",
+    "libutils",
+    "libbase",
+    "libbrotli",
+    "libbz",
+    "libcrypto",
+    "libz",
+    "libziparchive",
+]
+
+// librecovery_defaults uses many shared libs that we want to avoid using in tests (e.g. we don't
+// have 32-bit android.hardware.health@2.0.so or libbootloader_message.so on marlin).
+librecovery_static_libs = [
+    "librecovery",
+    "librecovery_fastboot",
+    "libminui",
+    "libverifier",
+    "libotautil",
+
+    "libhealthhalutils",
+    "libvintf_recovery",
+    "libvintf",
+
+    "android.hardware.health@2.0",
+    "android.hardware.health@1.0",
+    "libbootloader_message",
+    "libext4_utils",
+    "libfs_mgr",
+    "libfusesideload",
+    "libhidl-gen-utils",
+    "libhidlbase",
+    "libhidltransport",
+    "libhwbinder",
+    "libbinderthreadstate",
+    "libvndksupport",
+    "libtinyxml2",
+]
+
+cc_test {
+    name: "recovery_unit_test",
+    isolated: true,
+
+    defaults: [
+        "recovery_test_defaults",
+    ],
+
+    test_suites: ["device-tests"],
+
+    srcs: [
+        "unit/*.cpp",
+    ],
+
+    static_libs: libapplypatch_static_libs + [
+        "librecovery_ui",
+        "libminui",
+        "libverifier",
+        "libotautil",
+        "libupdater",
+        "libgtest_prod",
+    ],
+
+    data: ["testdata/*"],
+}
+
+cc_test {
+    name: "recovery_manual_test",
+    isolated: true,
+
+    defaults: [
+        "recovery_test_defaults",
+    ],
+
+    test_suites: ["device-tests"],
+
+    srcs: [
+        "manual/recovery_test.cpp",
+    ],
+}
+
+cc_test {
+    name: "recovery_component_test",
+    isolated: true,
+
+    defaults: [
+        "recovery_test_defaults",
+        "libupdater_defaults",
+    ],
+
+    test_suites: ["device-tests"],
+
+    srcs: [
+        "component/*.cpp",
+    ],
+
+    static_libs: libapplypatch_static_libs + librecovery_static_libs + [
+        "libupdater",
+        "libupdate_verifier",
+        "libprotobuf-cpp-lite",
+    ],
+
+    data: [
+        "testdata/*",
+        ":res-testdata",
+    ],
+}
+
+cc_test_host {
+    name: "recovery_host_test",
+    isolated: true,
+
+    defaults: [
+        "recovery_test_defaults",
+    ],
+
+    srcs: [
+        "component/imgdiff_test.cpp",
+    ],
+
+    static_libs: [
+        "libimgdiff",
+        "libimgpatch",
+        "libotautil",
+        "libbsdiff",
+        "libbspatch",
+        "libziparchive",
+        "libutils",
+        "libcrypto",
+        "libbrotli",
+        "libbz",
+        "libdivsufsort64",
+        "libdivsufsort",
+        "libz",
+    ],
+
+    data: ["testdata/*"],
+
+    target: {
+        darwin: {
+            // libimgdiff is not available on the Mac.
+            enabled: false,
+        },
+    },
+}
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index b3584fe..0000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1,223 +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)
-
-# Unit tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_unit_test
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_STATIC_LIBRARIES := \
-    libverifier \
-    libminui \
-    libotautil \
-    libupdater \
-    libziparchive \
-    libutils \
-    libz \
-    libselinux \
-    libbase \
-    libBionicGtestMain
-
-LOCAL_SRC_FILES := \
-    unit/asn1_decoder_test.cpp \
-    unit/dirutil_test.cpp \
-    unit/locale_test.cpp \
-    unit/rangeset_test.cpp \
-    unit/sysutil_test.cpp \
-    unit/zip_test.cpp \
-
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SHARED_LIBRARIES := liblog
-include $(BUILD_NATIVE_TEST)
-
-# Manual tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_manual_test
-LOCAL_STATIC_LIBRARIES := \
-    libminui \
-    libbase \
-    libBionicGtestMain
-
-LOCAL_SRC_FILES := manual/recovery_test.cpp
-LOCAL_SHARED_LIBRARIES := \
-    liblog \
-    libpng
-
-resource_files := $(call find-files-in-subdirs, bootable/recovery, \
-    "*_text.png", \
-    res-mdpi/images \
-    res-hdpi/images \
-    res-xhdpi/images \
-    res-xxhdpi/images \
-    res-xxxhdpi/images \
-    )
-
-# The resource image files that will go to $OUT/data/nativetest/recovery.
-testimage_out_path := $(TARGET_OUT_DATA)/nativetest/recovery
-GEN := $(addprefix $(testimage_out_path)/, $(resource_files))
-
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testimage_out_path)/% : bootable/recovery/%
-	$(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-
-include $(BUILD_NATIVE_TEST)
-
-# Component tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := \
-    -Wall \
-    -Werror \
-    -D_FILE_OFFSET_BITS=64
-
-ifeq ($(AB_OTA_UPDATER),true)
-LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
-endif
-
-ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true)
-LOCAL_CFLAGS += -DPRODUCT_SUPPORTS_VERITY=1
-endif
-
-ifeq ($(BOARD_AVB_ENABLE),true)
-LOCAL_CFLAGS += -DBOARD_AVB_ENABLE=1
-endif
-
-LOCAL_MODULE := recovery_component_test
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SRC_FILES := \
-    component/applypatch_test.cpp \
-    component/bootloader_message_test.cpp \
-    component/edify_test.cpp \
-    component/imgdiff_test.cpp \
-    component/install_test.cpp \
-    component/sideload_test.cpp \
-    component/uncrypt_test.cpp \
-    component/updater_test.cpp \
-    component/update_verifier_test.cpp \
-    component/verifier_test.cpp
-
-LOCAL_SHARED_LIBRARIES := \
-    libhidlbase
-
-tune2fs_static_libraries := \
-    libext2_com_err \
-    libext2_blkid \
-    libext2_quota \
-    libext2_uuid \
-    libext2_e2p \
-    libext2fs
-
-LOCAL_STATIC_LIBRARIES := \
-    libapplypatch_modes \
-    libapplypatch \
-    libedify \
-    libimgdiff \
-    libimgpatch \
-    libbsdiff \
-    libbspatch \
-    libfusesideload \
-    libotafault \
-    librecovery \
-    libupdater \
-    libbootloader_message \
-    libverifier \
-    libotautil \
-    libmounts \
-    libupdate_verifier \
-    libdivsufsort \
-    libdivsufsort64 \
-    libfs_mgr \
-    libvintf_recovery \
-    libvintf \
-    libhidl-gen-utils \
-    libtinyxml2 \
-    libselinux \
-    libext4_utils \
-    libsparse \
-    libcrypto_utils \
-    libcrypto \
-    libbz \
-    libziparchive \
-    liblog \
-    libutils \
-    libz \
-    libbase \
-    libtune2fs \
-    libfec \
-    libfec_rs \
-    libsquashfs_utils \
-    libcutils \
-    libbrotli \
-    libBionicGtestMain \
-    $(tune2fs_static_libraries)
-
-testdata_files := $(call find-subdir-files, testdata/*)
-
-# The testdata files that will go to $OUT/data/nativetest/recovery.
-testdata_out_path := $(TARGET_OUT_DATA)/nativetest/recovery
-GEN := $(addprefix $(testdata_out_path)/, $(testdata_files))
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testdata_out_path)/% : $(LOCAL_PATH)/%
-	$(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-
-# A copy of the testdata to be packed into continuous_native_tests.zip.
-testdata_continuous_zip_prefix := \
-    $(call intermediates-dir-for,PACKAGING,recovery_component_test)/DATA
-testdata_continuous_zip_path := $(testdata_continuous_zip_prefix)/nativetest/recovery
-GEN := $(addprefix $(testdata_continuous_zip_path)/, $(testdata_files))
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testdata_continuous_zip_path)/% : $(LOCAL_PATH)/%
-	$(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-LOCAL_PICKUP_FILES := $(testdata_continuous_zip_prefix)
-
-include $(BUILD_NATIVE_TEST)
-
-# Host tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_host_test
-LOCAL_MODULE_HOST_OS := linux
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SRC_FILES := \
-    component/imgdiff_test.cpp
-LOCAL_STATIC_LIBRARIES := \
-    libimgdiff \
-    libimgpatch \
-    libotautil \
-    libbsdiff \
-    libbspatch \
-    libziparchive \
-    libutils \
-    libbase \
-    libcrypto \
-    libbrotli \
-    libbz \
-    libdivsufsort64 \
-    libdivsufsort \
-    libz \
-    libBionicGtestMain
-LOCAL_SHARED_LIBRARIES := \
-    liblog
-include $(BUILD_HOST_NATIVE_TEST)
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index 3999aa5..6b86085 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -16,16 +16,18 @@
 <configuration description="Config for recovery_component_test and recovery_unit_test">
     <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
         <option name="cleanup" value="true" />
-        <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test" />
-        <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test" />
+        <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test/recovery_component_test" />
+        <option name="push" value="testdata->/data/local/tmp/recovery_component_test/testdata" />
+        <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test/recovery_unit_test" />
+        <option name="push" value="testdata->/data/local/tmp/recovery_unit_test/testdata" />
     </target_preparer>
     <option name="test-suite-tag" value="apct" />
     <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="native-test-device-path" value="/data/local/tmp/recovery_component_test" />
         <option name="module-name" value="recovery_component_test" />
     </test>
     <test class="com.android.tradefed.testtype.GTest" >
-        <option name="native-test-device-path" value="/data/local/tmp" />
+        <option name="native-test-device-path" value="/data/local/tmp/recovery_unit_test" />
         <option name="module-name" value="recovery_unit_test" />
     </test>
 </configuration>
diff --git a/tests/common/test_constants.h b/tests/common/test_constants.h
index 514818e..b6c27a7 100644
--- a/tests/common/test_constants.h
+++ b/tests/common/test_constants.h
@@ -17,10 +17,10 @@
 #ifndef _OTA_TEST_CONSTANTS_H
 #define _OTA_TEST_CONSTANTS_H
 
-#include <stdlib.h>
-
 #include <string>
 
+#include <android-base/file.h>
+
 // Zip entries in ziptest_valid.zip.
 static const std::string kATxtContents("abcdefghabcdefgh\n");
 static const std::string kBTxtContents("abcdefgh\n");
@@ -32,14 +32,9 @@
 // echo -n -e "abcdefgh\n" | sha1sum
 static const std::string kBTxtSha1Sum("e414af7161c9554089f4106d6f1797ef14a73666");
 
-static std::string from_testdata_base(const std::string& 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;
+[[maybe_unused]] static std::string from_testdata_base(const std::string& fname) {
+  static std::string exec_dir = android::base::GetExecutableDirectory();
+  return exec_dir + "/testdata/" + fname;
 }
 
 #endif  // _OTA_TEST_CONSTANTS_H
diff --git a/tests/component/applypatch_modes_test.cpp b/tests/component/applypatch_modes_test.cpp
new file mode 100644
index 0000000..ce01f4f
--- /dev/null
+++ b/tests/component/applypatch_modes_test.cpp
@@ -0,0 +1,199 @@
+/*
+ * 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 agree to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
+#include <bsdiff/bsdiff.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "applypatch/applypatch_modes.h"
+#include "common/test_constants.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+
+using namespace std::string_literals;
+
+// Loads a given partition and returns a string of form "EMMC:name:size:hash".
+static std::string GetEmmcTargetString(const std::string& filename,
+                                       const std::string& display_name = "") {
+  std::string data;
+  if (!android::base::ReadFileToString(filename, &data)) {
+    PLOG(ERROR) << "Failed to read " << filename;
+    return {};
+  }
+
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
+
+  return "EMMC:"s + (display_name.empty() ? filename : display_name) + ":" +
+         std::to_string(data.size()) + ":" + print_sha1(digest);
+}
+
+class ApplyPatchModesTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    source = GetEmmcTargetString(from_testdata_base("boot.img"));
+    ASSERT_FALSE(source.empty());
+
+    std::string recovery_file = from_testdata_base("recovery.img");
+    recovery = GetEmmcTargetString(recovery_file);
+    ASSERT_FALSE(recovery.empty());
+
+    ASSERT_TRUE(android::base::WriteStringToFile("", patched_file_.path));
+    target = GetEmmcTargetString(recovery_file, patched_file_.path);
+    ASSERT_FALSE(target.empty());
+
+    Paths::Get().set_cache_temp_source(cache_source_.path);
+  }
+
+  std::string source;
+  std::string target;
+  std::string recovery;
+
+ private:
+  TemporaryFile cache_source_;
+  TemporaryFile patched_file_;
+};
+
+static int InvokeApplyPatchModes(const std::vector<std::string>& args) {
+  auto args_to_call = StringVectorToNullTerminatedArray(args);
+  return applypatch_modes(args_to_call.size() - 1, args_to_call.data());
+}
+
+static void VerifyPatchedTarget(const std::string& target) {
+  std::vector<std::string> pieces = android::base::Split(target, ":");
+  ASSERT_EQ(4, pieces.size());
+  ASSERT_EQ("EMMC", pieces[0]);
+
+  std::string patched_emmc = GetEmmcTargetString(pieces[1]);
+  ASSERT_FALSE(patched_emmc.empty());
+  ASSERT_EQ(target, patched_emmc);
+}
+
+TEST_F(ApplyPatchModesTest, InvalidArgs) {
+  // At least two args (including the filename).
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch" }));
+
+  // Unrecognized args.
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "-x" }));
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
+  std::vector<std::string> args{
+    "applypatch",
+    "--bonus",
+    from_testdata_base("bonus.file"),
+    "--patch",
+    from_testdata_base("recovery-from-boot.p"),
+    "--target",
+    target,
+    "--source",
+    source,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithoutBonusFile) {
+  std::vector<std::string> args{
+    "applypatch", "--patch", from_testdata_base("recovery-from-boot-with-bonus.p"),
+    "--target",   target,    "--source",
+    source,
+  };
+
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) {
+  // Generate the bsdiff patch of recovery-from-boot.p.
+  std::string src_content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("boot.img"), &src_content));
+
+  std::string tgt_content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("recovery.img"), &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));
+
+  std::vector<std::string> args{
+    "applypatch", "--patch", patch_file.path, "--target", target, "--source", source,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) {
+  // Invalid bonus file.
+  std::vector<std::string> args{
+    "applypatch", "--bonus", "/doesntexist", "--patch", from_testdata_base("recovery-from-boot.p"),
+    "--target",   target,    "--source",     source,
+  };
+  ASSERT_NE(0, InvokeApplyPatchModes(args));
+
+  // With bonus file, but missing args.
+  ASSERT_NE(0,
+            InvokeApplyPatchModes({ "applypatch", "--bonus", from_testdata_base("bonus.file") }));
+}
+
+TEST_F(ApplyPatchModesTest, FlashMode) {
+  std::vector<std::string> args{
+    "applypatch", "--flash", from_testdata_base("recovery.img"), "--target", target,
+  };
+  ASSERT_EQ(0, InvokeApplyPatchModes(args));
+  VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, FlashModeInvalidArgs) {
+  std::vector<std::string> args{
+    "applypatch", "--bonus", from_testdata_base("bonus.file"), "--flash", source,
+    "--target",   target,
+  };
+  ASSERT_NE(0, InvokeApplyPatchModes(args));
+}
+
+TEST_F(ApplyPatchModesTest, CheckMode) {
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", recovery }));
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", source }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
+  ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "--check" }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeNonEmmcTarget) {
+  ASSERT_NE(0, InvokeApplyPatchModes({ "applypatch", "--check", from_testdata_base("boot.img") }));
+}
+
+TEST_F(ApplyPatchModesTest, ShowLicenses) {
+  ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--license" }));
+}
diff --git a/tests/component/applypatch_test.cpp b/tests/component/applypatch_test.cpp
deleted file mode 100644
index 61e06ad..0000000
--- a/tests/component/applypatch_test.cpp
+++ /dev/null
@@ -1,387 +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 agree to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <fcntl.h>
-#include <gtest/gtest.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/stat.h>
-#include <sys/statvfs.h>
-#include <sys/types.h>
-#include <time.h>
-
-#include <memory>
-#include <string>
-#include <vector>
-
-#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 "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);
-
-  std::string data;
-  ASSERT_TRUE(android::base::ReadFileToString(fname, &data));
-
-  if (fsize != nullptr) {
-    *fsize = data.size();
-  }
-
-  uint8_t digest[SHA_DIGEST_LENGTH];
-  SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
-  *sha1 = print_sha1(digest);
-}
-
-static void mangle_file(const std::string& fname) {
-  std::string content(1024, '\0');
-  for (size_t i = 0; i < 1024; i++) {
-    content[i] = rand() % 256;
-  }
-  ASSERT_TRUE(android::base::WriteStringToFile(content, fname));
-}
-
-class ApplyPatchTest : public ::testing::Test {
- public:
-  virtual void SetUp() override {
-    // set up files
-    old_file = from_testdata_base("old.file");
-    new_file = from_testdata_base("new.file");
-    nonexistent_file = from_testdata_base("nonexistent.file");
-
-    // set up SHA constants
-    sha1sum(old_file, &old_sha1, &old_size);
-    sha1sum(new_file, &new_sha1, &new_size);
-    srand(time(nullptr));
-    bad_sha1_a = android::base::StringPrintf("%040x", rand());
-    bad_sha1_b = android::base::StringPrintf("%040x", rand());
-  }
-
-  std::string old_file;
-  std::string new_file;
-  std::string nonexistent_file;
-
-  std::string old_sha1;
-  std::string new_sha1;
-  std::string bad_sha1_a;
-  std::string bad_sha1_b;
-
-  size_t old_size;
-  size_t new_size;
-};
-
-class ApplyPatchCacheTest : public ApplyPatchTest {
- protected:
-  void SetUp() override {
-    ApplyPatchTest::SetUp();
-    CacheLocation::location().set_cache_temp_source(old_file);
-  }
-};
-
-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;
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeSingle) {
-  std::vector<std::string> sha1s = { old_sha1 };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeMultiple) {
-  std::vector<std::string> sha1s = { bad_sha1_a, old_sha1, bad_sha1_b };
-  ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeFailure) {
-  std::vector<std::string> sha1s = { bad_sha1_a, bad_sha1_b };
-  ASSERT_NE(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeEmmcTarget) {
-  // EMMC:old_file:size:sha1 should pass the check.
-  std::string src_file =
-      "EMMC:" + old_file + ":" + std::to_string(old_size) + ":" + old_sha1;
-  std::vector<std::string> sha1s;
-  ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
-  // EMMC:old_file:(size-1):sha1:(size+1):sha1 should fail the check.
-  src_file = "EMMC:" + old_file + ":" + std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size + 1) + ":" + old_sha1;
-  ASSERT_EQ(1, applypatch_check(src_file.c_str(), sha1s));
-
-  // EMMC:old_file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check.
-  src_file = "EMMC:" + old_file + ":" +
-             std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size) + ":" + old_sha1 + ":" +
-             std::to_string(old_size + 1) + ":" + old_sha1;
-  ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
-  // EMMC:old_file:(size+1):sha1:(size-1):sha1:size:sha1 should pass the check.
-  src_file = "EMMC:" + old_file + ":" +
-             std::to_string(old_size + 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size) + ":" + old_sha1;
-  ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
-  // EMMC:new_file:(size+1):old_sha1:(size-1):old_sha1:size:old_sha1:size:new_sha1
-  // should pass the check.
-  src_file = "EMMC:" + new_file + ":" +
-             std::to_string(old_size + 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
-             std::to_string(old_size) + ":" + old_sha1 + ":" +
-             std::to_string(new_size) + ":" + new_sha1;
-  ASSERT_EQ(0, applypatch_check(src_file.c_str(), 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, 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, 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(ApplyPatchModesTest, InvalidArgs) {
-  // At least two args (including the filename).
-  ASSERT_EQ(2, applypatch_modes(1, (const char* []){ "applypatch" }));
-
-  // Unrecognized args.
-  ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-x" }));
-}
-
-TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
-  std::string boot_img = from_testdata_base("boot.img");
-  size_t boot_img_size;
-  std::string boot_img_sha1;
-  sha1sum(boot_img, &boot_img_sha1, &boot_img_size);
-
-  std::string recovery_img = from_testdata_base("recovery.img");
-  size_t size;
-  std::string recovery_img_sha1;
-  sha1sum(recovery_img, &recovery_img_sha1, &size);
-  std::string recovery_img_size = std::to_string(size);
-
-  std::string bonus_file = from_testdata_base("bonus.file");
-
-  // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
-  TemporaryFile tmp1;
-  std::string src_file =
-      "EMMC:" + boot_img + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1;
-  std::string tgt_file = "EMMC:" + std::string(tmp1.path);
-  std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p");
-  std::vector<const char*> args = {
-    "applypatch",
-    "-b",
-    bonus_file.c_str(),
-    src_file.c_str(),
-    tgt_file.c_str(),
-    recovery_img_sha1.c_str(),
-    recovery_img_size.c_str(),
-    patch.c_str()
-  };
-  ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
-
-  // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
-  TemporaryFile tmp2;
-  patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p");
-  tgt_file = "EMMC:" + std::string(tmp2.path);
-  std::vector<const char*> args2 = {
-    "applypatch",
-    src_file.c_str(),
-    tgt_file.c_str(),
-    recovery_img_sha1.c_str(),
-    recovery_img_size.c_str(),
-    patch.c_str()
-  };
-  ASSERT_EQ(0, applypatch_modes(args2.size(), args2.data()));
-
-  // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> \
-  //               <src-sha1-fake>:<patch1> <src-sha1>:<patch2>
-  TemporaryFile tmp3;
-  tgt_file = "EMMC:" + std::string(tmp3.path);
-  std::string bad_sha1_a = android::base::StringPrintf("%040x", rand());
-  std::string bad_sha1_b = android::base::StringPrintf("%040x", rand());
-  std::string patch1 = bad_sha1_a + ":" + from_testdata_base("recovery-from-boot.p");
-  std::string patch2 = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p");
-  std::string patch3 = bad_sha1_b + ":" + from_testdata_base("recovery-from-boot.p");
-  std::vector<const char*> args3 = {
-    "applypatch",
-    "-b",
-    bonus_file.c_str(),
-    src_file.c_str(),
-    tgt_file.c_str(),
-    recovery_img_sha1.c_str(),
-    recovery_img_size.c_str(),
-    patch1.c_str(),
-    patch2.c_str(),
-    patch3.c_str()
-  };
-  ASSERT_EQ(0, applypatch_modes(args3.size(), args3.data()));
-}
-
-// 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" }));
-
-  std::string bonus_file = from_testdata_base("bonus.file");
-  // With bonus file, but missing args.
-  ASSERT_EQ(2, applypatch_modes(3, (const char* []){ "applypatch", "-b", bonus_file.c_str() }));
-
-  std::string boot_img = from_testdata_base("boot.img");
-  size_t boot_img_size;
-  std::string boot_img_sha1;
-  sha1sum(boot_img, &boot_img_sha1, &boot_img_size);
-
-  std::string recovery_img = from_testdata_base("recovery.img");
-  size_t size;
-  std::string recovery_img_sha1;
-  sha1sum(recovery_img, &recovery_img_sha1, &size);
-  std::string recovery_img_size = std::to_string(size);
-
-  // Bonus file is not supported in flash mode.
-  // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size>
-  TemporaryFile tmp4;
-  std::vector<const char*> args4 = {
-    "applypatch",
-    "-b",
-    bonus_file.c_str(),
-    boot_img.c_str(),
-    tmp4.path,
-    recovery_img_sha1.c_str(),
-    recovery_img_size.c_str()
-  };
-  ASSERT_NE(0, applypatch_modes(args4.size(), args4.data()));
-
-  // Failed to parse patch args.
-  TemporaryFile tmp5;
-  std::string bad_arg1 =
-      "invalid-sha1:filename" + from_testdata_base("recovery-from-boot-with-bonus.p");
-  std::vector<const char*> args5 = {
-    "applypatch",
-    boot_img.c_str(),
-    tmp5.path,
-    recovery_img_sha1.c_str(),
-    recovery_img_size.c_str(),
-    bad_arg1.c_str()
-  };
-  ASSERT_NE(0, applypatch_modes(args5.size(), args5.data()));
-
-  // Target size cannot be zero.
-  TemporaryFile tmp6;
-  std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p");
-  std::vector<const char*> args6 = {
-    "applypatch",
-    boot_img.c_str(),
-    tmp6.path,
-    recovery_img_sha1.c_str(),
-    "0",  // target size
-    patch.c_str()
-  };
-  ASSERT_NE(0, applypatch_modes(args6.size(), args6.data()));
-}
-
-TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
-  // Insufficient args.
-  ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-c" }));
-}
-
-TEST_F(ApplyPatchModesTest, ShowLicenses) {
-  ASSERT_EQ(0, applypatch_modes(2, (const char* []){ "applypatch", "-l" }));
-}
diff --git a/tests/component/edify_test.cpp b/tests/component/edify_test.cpp
index 61a1e6b..8397bd3 100644
--- a/tests/component/edify_test.cpp
+++ b/tests/component/edify_test.cpp
@@ -21,30 +21,29 @@
 
 #include "edify/expr.h"
 
-static void expect(const char* expr_str, const char* expected) {
-    std::unique_ptr<Expr> e;
-    int error_count = 0;
-    EXPECT_EQ(0, parse_string(expr_str, &e, &error_count));
-    EXPECT_EQ(0, error_count);
+static void expect(const std::string& expr_str, const char* expected) {
+  std::unique_ptr<Expr> e;
+  int error_count = 0;
+  EXPECT_EQ(0, ParseString(expr_str, &e, &error_count));
+  EXPECT_EQ(0, error_count);
 
-    State state(expr_str, nullptr);
+  State state(expr_str, nullptr);
 
-    std::string result;
-    bool status = Evaluate(&state, e, &result);
+  std::string result;
+  bool status = Evaluate(&state, e, &result);
 
-    if (expected == nullptr) {
-        EXPECT_FALSE(status);
-    } else {
-        EXPECT_STREQ(expected, result.c_str());
-    }
-
+  if (expected == nullptr) {
+    EXPECT_FALSE(status);
+  } else {
+    EXPECT_STREQ(expected, result.c_str());
+  }
 }
 
 class EdifyTest : public ::testing::Test {
-  protected:
-    virtual void SetUp() {
-        RegisterBuiltins();
-    }
+ protected:
+  void SetUp() {
+    RegisterBuiltins();
+  }
 };
 
 TEST_F(EdifyTest, parsing) {
@@ -146,25 +145,23 @@
 }
 
 TEST_F(EdifyTest, big_string) {
-    // big string
-    expect(std::string(8192, 's').c_str(), std::string(8192, 's').c_str());
+  expect(std::string(8192, 's'), std::string(8192, 's').c_str());
 }
 
 TEST_F(EdifyTest, unknown_function) {
-    // unknown function
-    const char* script1 = "unknown_function()";
-    std::unique_ptr<Expr> expr;
-    int error_count = 0;
-    EXPECT_EQ(1, parse_string(script1, &expr, &error_count));
-    EXPECT_EQ(1, error_count);
+  const char* script1 = "unknown_function()";
+  std::unique_ptr<Expr> expr;
+  int error_count = 0;
+  EXPECT_EQ(1, ParseString(script1, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
 
-    const char* script2 = "abc; unknown_function()";
-    error_count = 0;
-    EXPECT_EQ(1, parse_string(script2, &expr, &error_count));
-    EXPECT_EQ(1, error_count);
+  const char* script2 = "abc; unknown_function()";
+  error_count = 0;
+  EXPECT_EQ(1, ParseString(script2, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
 
-    const char* script3 = "unknown_function1() || yes";
-    error_count = 0;
-    EXPECT_EQ(1, parse_string(script3, &expr, &error_count));
-    EXPECT_EQ(1, error_count);
+  const char* script3 = "unknown_function1() || yes";
+  error_count = 0;
+  EXPECT_EQ(1, ParseString(script3, &expr, &error_count));
+  EXPECT_EQ(1, error_count);
 }
diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp
index 6c23def..cb4868a 100644
--- a/tests/component/imgdiff_test.cpp
+++ b/tests/component/imgdiff_test.cpp
@@ -197,12 +197,17 @@
 }
 
 TEST(ImgdiffTest, zip_mode_smoke_compressed) {
+  // Generate 1 block of random data.
+  std::string random_data;
+  random_data.reserve(4096);
+  generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
   // Construct src and tgt zip files.
   TemporaryFile src_file;
   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");
+  const std::string src_content = random_data;
   ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
   ASSERT_EQ(0, src_writer.FinishEntry());
   ASSERT_EQ(0, src_writer.Finish());
@@ -212,7 +217,7 @@
   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");
+  const std::string tgt_content = random_data + "extra contents";
   ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
   ASSERT_EQ(0, tgt_writer.FinishEntry());
   ASSERT_EQ(0, tgt_writer.Finish());
@@ -245,13 +250,57 @@
   verify_patched_image(src, patch, tgt);
 }
 
+TEST(ImgdiffTest, zip_mode_empty_target) {
+  TemporaryFile src_file;
+  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";
+  ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+  ASSERT_EQ(0, src_writer.FinishEntry());
+  ASSERT_EQ(0, src_writer.Finish());
+  ASSERT_EQ(0, fclose(src_file_ptr));
+
+  // Construct a empty entry in the target zip.
+  TemporaryFile tgt_file;
+  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;
+  ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+  ASSERT_EQ(0, tgt_writer.FinishEntry());
+  ASSERT_EQ(0, tgt_writer.Finish());
+
+  // Compute patch.
+  TemporaryFile patch_file;
+  std::vector<const char*> args = {
+    "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+  };
+  ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+  // Verify.
+  std::string tgt;
+  ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+  std::string src;
+  ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+  std::string patch;
+  ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+  verify_patched_image(src, patch, tgt);
+}
+
 TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) {
+  // Generate 1 block of random data.
+  std::string random_data;
+  random_data.reserve(4096);
+  generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
   // Construct src and tgt zip files.
   TemporaryFile src_file;
   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");
+  const std::string src_content = random_data;
   ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
   ASSERT_EQ(0, src_writer.FinishEntry());
   ASSERT_EQ(0, src_writer.Finish());
@@ -261,7 +310,7 @@
   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");
+  const std::string tgt_content = random_data + "abcdefg";
   ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
   ASSERT_EQ(0, tgt_writer.FinishEntry());
   ASSERT_EQ(0, tgt_writer.Finish());
@@ -298,23 +347,19 @@
 }
 
 TEST(ImgdiffTest, image_mode_simple) {
-  // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd).
-  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
-                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
-                                       '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
-                                       '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
-                                       '\x00', '\x00', '\x00' };
-  const std::string src(src_data.cbegin(), src_data.cend());
+  std::string gzipped_source_path = from_testdata_base("gzipped_source");
+  std::string gzipped_source;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+  const std::string src = "abcdefg" + gzipped_source;
   TemporaryFile src_file;
   ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
 
-  // tgt: "abcdefgxyz" + gzipped "xxyyzz".
-  const std::vector<char> tgt_data = {
-    'a',    'b',    'c',    'd',    'e',    'f',    'g',    'x',    'y',    'z',    '\x1f', '\x8b',
-    '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac',
-    '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00'
-  };
-  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  std::string gzipped_target_path = from_testdata_base("gzipped_target");
+  std::string gzipped_target;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+  const std::string tgt = "abcdefgxyz" + gzipped_target;
+
   TemporaryFile tgt_file;
   ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
 
@@ -404,23 +449,21 @@
 }
 
 TEST(ImgdiffTest, image_mode_merge_chunks) {
-  // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd).
-  const std::vector<char> src_data = { 'a',    'b',    'c',    'd',    'e',    'f',    'g',
-                                       'h',    '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
-                                       '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
-                                       '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
-                                       '\x00', '\x00', '\x00' };
-  const std::string src(src_data.cbegin(), src_data.cend());
+  // src: "abcdefg" + gzipped_source.
+  std::string gzipped_source_path = from_testdata_base("gzipped_source");
+  std::string gzipped_source;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+  const std::string src = "abcdefg" + gzipped_source;
   TemporaryFile src_file;
   ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
 
-  // tgt: gzipped "xyz" + "abcdefgh".
-  const std::vector<char> tgt_data = {
-    '\x1f', '\x8b', '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8',
-    '\xa8', '\xac', '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00',
-    '\x00', '\x00', 'a',    'b',    'c',    'd',    'e',    'f',    'g',    'x',    'y',    'z'
-  };
-  const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+  // tgt: gzipped_target + "abcdefgxyz".
+  std::string gzipped_target_path = from_testdata_base("gzipped_target");
+  std::string gzipped_target;
+  ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+
+  const std::string tgt = gzipped_target + "abcdefgxyz";
   TemporaryFile tgt_file;
   ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
 
diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp
index d19d788..08b4290 100644
--- a/tests/component/install_test.cpp
+++ b/tests/component/install_test.cpp
@@ -33,6 +33,7 @@
 #include <ziparchive/zip_writer.h>
 
 #include "install.h"
+#include "otautil/paths.h"
 #include "private/install.h"
 
 TEST(InstallTest, verify_package_compatibility_no_entry) {
@@ -199,8 +200,73 @@
   CloseArchive(zip);
 }
 
-#ifdef AB_OTA_UPDATER
-static void VerifyAbUpdateBinaryCommand(const std::string& serialno, bool success = true) {
+TEST(InstallTest, SetUpNonAbUpdateCommands) {
+  TemporaryFile temp_file;
+  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));
+  ASSERT_EQ(0, writer.FinishEntry());
+  ASSERT_EQ(0, writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file));
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  int status_fd = 10;
+  std::string package = "/path/to/update.zip";
+  TemporaryDir td;
+  std::string binary_path = std::string(td.path) + "/update_binary";
+  Paths::Get().set_temporary_update_binary(binary_path);
+  std::vector<std::string> cmd;
+  ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+  ASSERT_EQ(4U, cmd.size());
+  ASSERT_EQ(binary_path, cmd[0]);
+  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
+  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+  ASSERT_EQ(package, cmd[3]);
+  struct stat sb;
+  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+  // With non-zero retry count. update_binary will be removed automatically.
+  cmd.clear();
+  ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 2, status_fd, &cmd));
+  ASSERT_EQ(5U, cmd.size());
+  ASSERT_EQ(binary_path, cmd[0]);
+  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
+  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+  ASSERT_EQ(package, cmd[3]);
+  ASSERT_EQ("retry", cmd[4]);
+  sb = {};
+  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) {
+  TemporaryFile temp_file;
+  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));
+  ASSERT_EQ(0, writer.FinishEntry());
+  ASSERT_EQ(0, writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file));
+
+  // Missing update binary.
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  int status_fd = 10;
+  std::string package = "/path/to/update.zip";
+  TemporaryDir td;
+  Paths::Get().set_temporary_update_binary(std::string(td.path) + "/update_binary");
+  std::vector<std::string> cmd;
+  ASSERT_EQ(INSTALL_CORRUPT, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+  CloseArchive(zip);
+}
+
+static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) {
   TemporaryFile temp_file;
   FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
@@ -235,73 +301,27 @@
   ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry));
   int status_fd = 10;
   std::string package = "/path/to/update.zip";
-  std::string binary_path = "/sbin/update_engine_sideload";
   std::vector<std::string> cmd;
   if (success) {
-    ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+    ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
     ASSERT_EQ(5U, cmd.size());
-    ASSERT_EQ(binary_path, cmd[0]);
+    ASSERT_EQ("/system/bin/update_engine_sideload", 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));
+    ASSERT_EQ(INSTALL_ERROR, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
   }
   CloseArchive(zip);
 }
-#endif  // AB_OTA_UPDATER
 
-TEST(InstallTest, update_binary_command_smoke) {
-#ifdef AB_OTA_UPDATER
+TEST(InstallTest, SetUpAbUpdateCommands) {
   // Empty serialno will pass the verification.
-  VerifyAbUpdateBinaryCommand({});
-#else
-  TemporaryFile temp_file;
-  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));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
-
-  ZipArchiveHandle zip;
-  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
-  int status_fd = 10;
-  std::string package = "/path/to/update.zip";
-  TemporaryDir td;
-  std::string binary_path = std::string(td.path) + "/update_binary";
-  std::vector<std::string> cmd;
-  ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
-  ASSERT_EQ(4U, cmd.size());
-  ASSERT_EQ(binary_path, cmd[0]);
-  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
-  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
-  ASSERT_EQ(package, cmd[3]);
-  struct stat sb;
-  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
-  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-
-  // With non-zero retry count. update_binary will be removed automatically.
-  cmd.clear();
-  ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 2, status_fd, &cmd));
-  ASSERT_EQ(5U, cmd.size());
-  ASSERT_EQ(binary_path, cmd[0]);
-  ASSERT_EQ("3", cmd[1]);  // RECOVERY_API_VERSION
-  ASSERT_EQ(std::to_string(status_fd), cmd[2]);
-  ASSERT_EQ(package, cmd[3]);
-  ASSERT_EQ("retry", cmd[4]);
-  sb = {};
-  ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
-  ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-
-  CloseArchive(zip);
-#endif  // AB_OTA_UPDATER
+  VerifyAbUpdateCommands({});
 }
 
-TEST(InstallTest, update_binary_command_invalid) {
-#ifdef AB_OTA_UPDATER
+TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) {
   TemporaryFile temp_file;
   FILE* zip_file = fdopen(temp_file.release(), "w");
   ZipWriter writer(zip_file);
@@ -328,60 +348,36 @@
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
   int status_fd = 10;
   std::string package = "/path/to/update.zip";
-  std::string binary_path = "/sbin/update_engine_sideload";
   std::vector<std::string> cmd;
-  ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+  ASSERT_EQ(INSTALL_CORRUPT, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
   CloseArchive(zip);
-#else
-  TemporaryFile temp_file;
-  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));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
-
-  // Missing update binary.
-  ZipArchiveHandle zip;
-  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
-  int status_fd = 10;
-  std::string package = "/path/to/update.zip";
-  TemporaryDir td;
-  std::string binary_path = std::string(td.path) + "/update_binary";
-  std::vector<std::string> cmd;
-  ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
-  CloseArchive(zip);
-#endif  // AB_OTA_UPDATER
 }
 
-#ifdef AB_OTA_UPDATER
-TEST(InstallTest, update_binary_command_multiple_serialno) {
+TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) {
   std::string serialno = android::base::GetProperty("ro.serialno", "");
   ASSERT_NE("", serialno);
 
   // Single matching serialno will pass the verification.
-  VerifyAbUpdateBinaryCommand(serialno);
+  VerifyAbUpdateCommands(serialno);
 
   static constexpr char alphabet[] =
       "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
   auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; };
 
   // Generate 900 random serial numbers.
-  std::string random_serial;
+  std::string random_serialno;
   for (size_t i = 0; i < 900; i++) {
-    generate_n(back_inserter(random_serial), serialno.size(), generator);
-    random_serial.append("|");
+    generate_n(back_inserter(random_serialno), serialno.size(), generator);
+    random_serialno.append("|");
   }
   // Random serialnos should fail the verification.
-  VerifyAbUpdateBinaryCommand(random_serial, false);
+  VerifyAbUpdateCommands(random_serialno, false);
 
-  std::string long_serial = random_serial + serialno + "|";
+  std::string long_serialno = random_serialno + serialno + "|";
   for (size_t i = 0; i < 99; i++) {
-    generate_n(back_inserter(long_serial), serialno.size(), generator);
-    long_serial.append("|");
+    generate_n(back_inserter(long_serialno), serialno.size(), generator);
+    long_serialno.append("|");
   }
   // String with the matching serialno should pass the verification.
-  VerifyAbUpdateBinaryCommand(long_serial);
+  VerifyAbUpdateCommands(long_serialno);
 }
-#endif  // AB_OTA_UPDATER
diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp
new file mode 100644
index 0000000..54329db
--- /dev/null
+++ b/tests/component/resources_test.cpp
@@ -0,0 +1,120 @@
+/*
+ * 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 <dirent.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <png.h>
+
+#include "minui/minui.h"
+#include "private/resources.h"
+
+static const std::string kLocale = "zu";
+
+static const std::vector<std::string> kResourceImagesDirs{
+  "res-mdpi/images/",   "res-hdpi/images/",    "res-xhdpi/images/",
+  "res-xxhdpi/images/", "res-xxxhdpi/images/",
+};
+
+static int png_filter(const dirent* de) {
+  if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) {
+    return 0;
+  }
+  return 1;
+}
+
+// Finds out all the PNG files to test, which stay under the same dir with the executabl..
+static std::vector<std::string> add_files() {
+  std::vector<std::string> files;
+  for (const std::string& images_dir : kResourceImagesDirs) {
+    static std::string exec_dir = android::base::GetExecutableDirectory();
+    std::string dir_path = exec_dir + "/" + images_dir;
+    dirent** namelist;
+    int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort);
+    if (n == -1) {
+      printf("Failed to scandir %s: %s\n", dir_path.c_str(), strerror(errno));
+      continue;
+    }
+    if (n == 0) {
+      printf("No file is added for test in %s\n", dir_path.c_str());
+    }
+
+    while (n--) {
+      std::string file_path = dir_path + namelist[n]->d_name;
+      files.push_back(file_path);
+      free(namelist[n]);
+    }
+    free(namelist);
+  }
+  return files;
+}
+
+class ResourcesTest : public testing::TestWithParam<std::string> {
+ public:
+  static std::vector<std::string> png_list;
+
+ protected:
+  void SetUp() override {
+    png_ = std::make_unique<PngHandler>(GetParam());
+    ASSERT_TRUE(png_);
+
+    ASSERT_EQ(PNG_COLOR_TYPE_GRAY, png_->color_type()) << "Recovery expects grayscale PNG file.";
+    ASSERT_LT(static_cast<png_uint_32>(5), png_->width());
+    ASSERT_LT(static_cast<png_uint_32>(0), png_->height());
+    ASSERT_EQ(1, png_->channels()) << "Recovery background text images expects 1-channel PNG file.";
+  }
+
+  std::unique_ptr<PngHandler> png_{ nullptr };
+};
+
+// Parses a png file and tests if it's qualified for the background text image under recovery.
+TEST_P(ResourcesTest, ValidateLocale) {
+  std::vector<unsigned char> row(png_->width());
+  for (png_uint_32 y = 0; y < png_->height(); ++y) {
+    png_read_row(png_->png_ptr(), row.data(), nullptr);
+    int w = (row[1] << 8) | row[0];
+    int h = (row[3] << 8) | row[2];
+    int len = row[4];
+    EXPECT_LT(0, w);
+    EXPECT_LT(0, h);
+    EXPECT_LT(0, len) << "Locale string should be non-empty.";
+    EXPECT_NE(0, row[5]) << "Locale string is missing.";
+
+    ASSERT_GT(png_->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));
+      break;
+    }
+    for (int i = 0; i < h; ++i, ++y) {
+      png_read_row(png_->png_ptr(), row.data(), nullptr);
+    }
+  }
+}
+
+std::vector<std::string> ResourcesTest::png_list = add_files();
+
+INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourcesTest,
+                        ::testing::ValuesIn(ResourcesTest::png_list.cbegin(),
+                                            ResourcesTest::png_list.cend()));
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index 1544bb2..2420c27 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -14,29 +14,89 @@
  * limitations under the License.
  */
 
+#include <update_verifier/update_verifier.h>
+
+#include <functional>
 #include <string>
+#include <unordered_map>
+#include <vector>
 
 #include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
 #include <android-base/test_utils.h>
+#include <google/protobuf/repeated_field.h>
 #include <gtest/gtest.h>
-#include <update_verifier/update_verifier.h>
+
+#include "care_map.pb.h"
+
+using namespace std::string_literals;
 
 class UpdateVerifierTest : public ::testing::Test {
  protected:
   void SetUp() override {
-#if defined(PRODUCT_SUPPORTS_VERITY) || defined(BOARD_AVB_ENABLE)
-    verity_supported = true;
-#else
-    verity_supported = false;
-#endif
+    std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
+    verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing");
+
+    care_map_prefix_ = care_map_dir_.path + "/care_map"s;
+    care_map_pb_ = care_map_dir_.path + "/care_map.pb"s;
+    care_map_txt_ = care_map_dir_.path + "/care_map.txt"s;
+    // Overrides the the care_map_prefix.
+    verifier_.set_care_map_prefix(care_map_prefix_);
+
+    property_id_ = "ro.build.fingerprint";
+    fingerprint_ = android::base::GetProperty(property_id_, "");
+    // Overrides the property_reader if we cannot read the given property on the device.
+    if (fingerprint_.empty()) {
+      fingerprint_ = "mock_fingerprint";
+      verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; });
+    }
+  }
+
+  void TearDown() override {
+    unlink(care_map_pb_.c_str());
+    unlink(care_map_txt_.c_str());
+  }
+
+  // Returns a serialized string of the proto3 message according to the given partition info.
+  std::string ConstructProto(
+      std::vector<std::unordered_map<std::string, std::string>>& partitions) {
+    recovery_update_verifier::CareMap result;
+    for (const auto& partition : partitions) {
+      recovery_update_verifier::CareMap::PartitionInfo info;
+      if (partition.find("name") != partition.end()) {
+        info.set_name(partition.at("name"));
+      }
+      if (partition.find("ranges") != partition.end()) {
+        info.set_ranges(partition.at("ranges"));
+      }
+      if (partition.find("id") != partition.end()) {
+        info.set_id(partition.at("id"));
+      }
+      if (partition.find("fingerprint") != partition.end()) {
+        info.set_fingerprint(partition.at("fingerprint"));
+      }
+
+      *result.add_partitions() = info;
+    }
+
+    return result.SerializeAsString();
   }
 
   bool verity_supported;
+  UpdateVerifier verifier_;
+
+  TemporaryDir care_map_dir_;
+  std::string care_map_prefix_;
+  std::string care_map_pb_;
+  std::string care_map_txt_;
+
+  std::string property_id_;
+  std::string fingerprint_;
 };
 
 TEST_F(UpdateVerifierTest, verify_image_no_care_map) {
-  // Non-existing care_map is allowed.
-  ASSERT_TRUE(verify_image("/doesntexist"));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_smoke) {
@@ -46,26 +106,28 @@
     return;
   }
 
-  TemporaryFile temp_file;
   std::string content = "system\n2,0,1";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
-  ASSERT_TRUE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
 
   // Leading and trailing newlines should be accepted.
-  ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", temp_file.path));
-  ASSERT_TRUE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", care_map_txt_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_empty_care_map) {
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_wrong_lines) {
   // The care map file can have only 2 / 4 / 6 lines.
-  TemporaryFile temp_file;
-  ASSERT_FALSE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("line1", care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 
-  ASSERT_TRUE(android::base::WriteStringToFile("line1", temp_file.path));
-  ASSERT_FALSE(verify_image(temp_file.path));
-
-  ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", temp_file.path));
-  ASSERT_FALSE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) {
@@ -75,10 +137,9 @@
     return;
   }
 
-  TemporaryFile temp_file;
   std::string content = "system\n2,1,0";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
-  ASSERT_FALSE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) {
@@ -88,8 +149,110 @@
     return;
   }
 
-  TemporaryFile temp_file;
   std::string content = "/dev/block/bootdevice/by-name/system\n2,1,0";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
-  ASSERT_TRUE(verify_image(temp_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "3,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", "unsupported_fingerprint" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index 5bfd7cb..24c63e7 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -27,6 +27,8 @@
 #include <vector>
 
 #include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
@@ -35,26 +37,33 @@
 #include <brotli/encode.h>
 #include <bsdiff/bsdiff.h>
 #include <gtest/gtest.h>
+#include <verity/hash_tree_builder.h>
 #include <ziparchive/zip_archive.h>
 #include <ziparchive/zip_writer.h>
 
+#include "applypatch/applypatch.h"
 #include "common/test_constants.h"
 #include "edify/expr.h"
-#include "otautil/SysUtil.h"
-#include "otautil/cache_location.h"
 #include "otautil/error_code.h"
+#include "otautil/paths.h"
 #include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+#include "private/commands.h"
 #include "updater/blockimg.h"
 #include "updater/install.h"
 #include "updater/updater.h"
 
-struct selabel_handle *sehandle = nullptr;
+using namespace std::string_literals;
 
-static void expect(const char* expected, const char* expr_str, CauseCode cause_code,
+using PackageEntries = std::unordered_map<std::string, std::string>;
+
+struct selabel_handle* sehandle = nullptr;
+
+static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code,
                    UpdaterInfo* info = nullptr) {
   std::unique_ptr<Expr> e;
   int error_count = 0;
-  ASSERT_EQ(0, parse_string(expr_str, &e, &error_count));
+  ASSERT_EQ(0, ParseString(expr_str, &e, &error_count));
   ASSERT_EQ(0, error_count);
 
   State state(expr_str, info);
@@ -65,7 +74,7 @@
   if (expected == nullptr) {
     ASSERT_FALSE(status);
   } else {
-    ASSERT_TRUE(status);
+    ASSERT_TRUE(status) << "Evaluate() finished with error message: " << state.errmsg;
     ASSERT_STREQ(expected, result.c_str());
   }
 
@@ -76,12 +85,12 @@
   ASSERT_EQ(cause_code, state.cause_code);
 }
 
-static void BuildUpdatePackage(const std::unordered_map<std::string, std::string>& entries,
-                               int fd) {
+static void BuildUpdatePackage(const PackageEntries& entries, int fd) {
   FILE* zip_file_ptr = fdopen(fd, "wb");
   ZipWriter zip_writer(zip_file_ptr);
 
   for (const auto& entry : entries) {
+    // All the entries are written as STORED.
     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()));
@@ -93,28 +102,103 @@
   ASSERT_EQ(0, fclose(zip_file_ptr));
 }
 
+static void RunBlockImageUpdate(bool is_verify, const PackageEntries& entries,
+                                const std::string& image_file, const std::string& result,
+                                CauseCode cause_code = kNoCause) {
+  CHECK(entries.find("transfer_list") != entries.end());
+
+  // 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 new_data = entries.find("new_data.br") != entries.end() ? "new_data.br" : "new_data";
+  std::string script = is_verify ? "block_image_verify" : "block_image_update";
+  script += R"((")" + image_file + R"(", package_extract_file("transfer_list"), ")" + new_data +
+            R"(", "patch_data"))";
+  expect(result.c_str(), script, cause_code, &updater_info);
+
+  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+  CloseArchive(handle);
+}
+
 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);
   return print_sha1(digest);
 }
 
+static Value* BlobToString(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());
+  }
+
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return nullptr;
+  }
+
+  if (args[0]->type != Value::Type::BLOB) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s() expects a BLOB argument", name);
+  }
+
+  args[0]->type = Value::Type::STRING;
+  return args[0].release();
+}
+
 class UpdaterTest : public ::testing::Test {
  protected:
-  virtual void SetUp() override {
+  void SetUp() override {
     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);
+    RegisterFunction("blob_to_string", BlobToString);
+
+    // Each test is run in a separate process (isolated mode). Shared temporary files won't cause
+    // conflicts.
+    Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+    Paths::Get().set_last_command_file(temp_last_command_.path);
+    Paths::Get().set_stash_directory_base(temp_stash_base_.path);
+
+    // Enable a special command "abort" to simulate interruption.
+    Command::abort_allowed_ = true;
+
+    last_command_file_ = temp_last_command_.path;
+    image_file_ = image_temp_file_.path;
+  }
+
+  void TearDown() override {
+    // Clean up the last_command_file if any.
+    ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+    // Clear partition updated marker if any.
+    std::string updated_marker{ temp_stash_base_.path };
+    updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+    ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
   }
 
   TemporaryFile temp_saved_source_;
-  TemporaryFile temp_last_command_;
   TemporaryDir temp_stash_base_;
+  std::string last_command_file_;
+  std::string image_file_;
+
+ private:
+  TemporaryFile temp_last_command_;
+  TemporaryFile image_temp_file_;
 };
 
 TEST_F(UpdaterTest, getprop) {
@@ -131,80 +215,47 @@
     expect(nullptr, "getprop(\"arg1\", \"arg2\")", kArgsParsingFailure);
 }
 
-TEST_F(UpdaterTest, sha1_check) {
-    // sha1_check(data) returns the SHA-1 of the data.
-    expect("81fe8bfe87576c3ecb22426f8e57847382917acf", "sha1_check(\"abcd\")", kNoCause);
-    expect("da39a3ee5e6b4b0d3255bfef95601890afd80709", "sha1_check(\"\")", kNoCause);
+TEST_F(UpdaterTest, patch_partition_check) {
+  // Zero argument is not valid.
+  expect(nullptr, "patch_partition_check()", kArgsParsingFailure);
 
-    // sha1_check(data, sha1_hex, [sha1_hex, ...]) returns the matched SHA-1.
-    expect("81fe8bfe87576c3ecb22426f8e57847382917acf",
-           "sha1_check(\"abcd\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")",
-           kNoCause);
+  std::string source_file = from_testdata_base("boot.img");
+  std::string source_content;
+  ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content));
+  size_t source_size = source_content.size();
+  std::string source_hash = get_sha1(source_content);
+  Partition source(source_file, source_size, source_hash);
 
-    expect("81fe8bfe87576c3ecb22426f8e57847382917acf",
-           "sha1_check(\"abcd\", \"wrong_sha1\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")",
-           kNoCause);
+  std::string target_file = from_testdata_base("recovery.img");
+  std::string target_content;
+  ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content));
+  size_t target_size = target_content.size();
+  std::string target_hash = get_sha1(target_content);
+  Partition target(target_file, target_size, target_hash);
 
-    // Or "" if there's no match.
-    expect("",
-           "sha1_check(\"abcd\", \"wrong_sha1\")",
-           kNoCause);
+  // One argument is not valid.
+  expect(nullptr, "patch_partition_check(\"" + source.ToString() + "\")", kArgsParsingFailure);
+  expect(nullptr, "patch_partition_check(\"" + target.ToString() + "\")", kArgsParsingFailure);
 
-    expect("",
-           "sha1_check(\"abcd\", \"wrong_sha1\", \"wrong_sha2\")",
-           kNoCause);
+  // Both of the source and target have the desired checksum.
+  std::string cmd =
+      "patch_partition_check(\"" + source.ToString() + "\", \"" + target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
 
-    // sha1_check() expects at least one argument.
-    expect(nullptr, "sha1_check()", kArgsParsingFailure);
-}
+  // Only source partition has the desired checksum.
+  Partition bad_target(target_file, target_size - 1, target_hash);
+  cmd = "patch_partition_check(\"" + source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
 
-TEST_F(UpdaterTest, apply_patch_check) {
-  // Zero-argument is not valid.
-  expect(nullptr, "apply_patch_check()", kArgsParsingFailure);
+  // Only target partition has the desired checksum.
+  Partition bad_source(source_file, source_size + 1, source_hash);
+  cmd = "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + target.ToString() + "\")";
+  expect("t", cmd, kNoCause);
 
-  // File not found.
-  expect("", "apply_patch_check(\"/doesntexist\")", kNoCause);
-
-  std::string src_file = from_testdata_base("old.file");
-  std::string src_content;
-  ASSERT_TRUE(android::base::ReadFileToString(src_file, &src_content));
-  size_t src_size = src_content.size();
-  std::string src_hash = get_sha1(src_content);
-
-  // One-argument with EMMC:file:size:sha1 should pass the check.
-  std::string filename = android::base::Join(
-      std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size), src_hash }, ":");
-  std::string cmd = "apply_patch_check(\"" + filename + "\")";
-  expect("t", cmd.c_str(), kNoCause);
-
-  // EMMC:file:(size-1):sha1:(size+1):sha1 should fail the check.
-  std::string filename_bad = android::base::Join(
-      std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1), src_hash,
-                                std::to_string(src_size + 1), src_hash },
-      ":");
-  cmd = "apply_patch_check(\"" + filename_bad + "\")";
-  expect("", cmd.c_str(), kNoCause);
-
-  // EMMC:file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check.
-  filename_bad =
-      android::base::Join(std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1),
-                                                    src_hash, std::to_string(src_size), src_hash,
-                                                    std::to_string(src_size + 1), src_hash },
-                          ":");
-  cmd = "apply_patch_check(\"" + filename_bad + "\")";
-  expect("t", cmd.c_str(), kNoCause);
-
-  // Multiple arguments.
-  cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"wrong_sha2\")";
-  expect("", cmd.c_str(), kNoCause);
-
-  cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"" + src_hash +
-        "\", \"wrong_sha2\")";
-  expect("t", cmd.c_str(), kNoCause);
-
-  cmd = "apply_patch_check(\"" + filename_bad + "\", \"wrong_sha1\", \"" + src_hash +
-        "\", \"wrong_sha2\")";
-  expect("t", cmd.c_str(), kNoCause);
+  // Neither of the source or target has the desired checksum.
+  cmd =
+      "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+  expect("", cmd, kNoCause);
 }
 
 TEST_F(UpdaterTest, file_getprop) {
@@ -214,7 +265,7 @@
     expect(nullptr, "file_getprop(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
 
     // File doesn't exist.
-    expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFileGetPropFailure);
+    expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFreadFailure);
 
     // Reject too large files (current limit = 65536).
     TemporaryFile temp_file1;
@@ -231,28 +282,28 @@
 
     std::string script1("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.name\")");
-    expect("tardis", script1.c_str(), kNoCause);
+    expect("tardis", script1, kNoCause);
 
     std::string script2("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.board\")");
-    expect("magic", script2.c_str(), kNoCause);
+    expect("magic", script2, kNoCause);
 
     // No match.
     std::string script3("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.wrong\")");
-    expect("", script3.c_str(), kNoCause);
+    expect("", script3, kNoCause);
 
     std::string script4("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.name=\")");
-    expect("", script4.c_str(), kNoCause);
+    expect("", script4, kNoCause);
 
     std::string script5("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.nam\")");
-    expect("", script5.c_str(), kNoCause);
+    expect("", script5, kNoCause);
 
     std::string script6("file_getprop(\"" + std::string(temp_file2.path) +
                        "\", \"ro.product.model\")");
-    expect("", script6.c_str(), kNoCause);
+    expect("", script6, kNoCause);
 }
 
 // TODO: Test extracting to block device.
@@ -272,7 +323,7 @@
   // Two-argument version.
   TemporaryFile temp_file1;
   std::string script("package_extract_file(\"a.txt\", \"" + std::string(temp_file1.path) + "\")");
-  expect("t", script.c_str(), kNoCause, &updater_info);
+  expect("t", script, kNoCause, &updater_info);
 
   // Verify the extracted entry.
   std::string data;
@@ -281,33 +332,135 @@
 
   // Now extract another entry to the same location, which should overwrite.
   script = "package_extract_file(\"b.txt\", \"" + std::string(temp_file1.path) + "\")";
-  expect("t", script.c_str(), kNoCause, &updater_info);
+  expect("t", script, kNoCause, &updater_info);
 
   ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data));
   ASSERT_EQ(kBTxtContents, data);
 
   // Missing zip entry. The two-argument version doesn't abort.
   script = "package_extract_file(\"doesntexist\", \"" + std::string(temp_file1.path) + "\")";
-  expect("", script.c_str(), kNoCause, &updater_info);
+  expect("", script, kNoCause, &updater_info);
 
   // Extract to /dev/full should fail.
   script = "package_extract_file(\"a.txt\", \"/dev/full\")";
-  expect("", script.c_str(), kNoCause, &updater_info);
+  expect("", script, kNoCause, &updater_info);
 
-  // One-argument version.
-  script = "sha1_check(package_extract_file(\"a.txt\"))";
-  expect(kATxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info);
+  // One-argument version. package_extract_file() gives a VAL_BLOB, which needs to be converted to
+  // VAL_STRING for equality test.
+  script = "blob_to_string(package_extract_file(\"a.txt\")) == \"" + kATxtContents + "\"";
+  expect("t", script, kNoCause, &updater_info);
 
-  script = "sha1_check(package_extract_file(\"b.txt\"))";
-  expect(kBTxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info);
+  script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\"";
+  expect("t", script, kNoCause, &updater_info);
 
   // Missing entry. The one-argument version aborts the evaluation.
   script = "package_extract_file(\"doesntexist\")";
-  expect(nullptr, script.c_str(), kPackageExtractFileFailure, &updater_info);
+  expect(nullptr, script, kPackageExtractFileFailure, &updater_info);
 
   CloseArchive(handle);
 }
 
+TEST_F(UpdaterTest, read_file) {
+  // read_file() expects one argument.
+  expect(nullptr, "read_file()", kArgsParsingFailure);
+  expect(nullptr, "read_file(\"arg1\", \"arg2\")", kArgsParsingFailure);
+
+  // Write some value to file and read back.
+  TemporaryFile temp_file;
+  std::string script("write_value(\"foo\", \""s + temp_file.path + "\");");
+  expect("t", script, kNoCause);
+
+  script = "read_file(\""s + temp_file.path + "\") == \"foo\"";
+  expect("t", script, kNoCause);
+
+  script = "read_file(\""s + temp_file.path + "\") == \"bar\"";
+  expect("", script, kNoCause);
+
+  // It should fail gracefully when read fails.
+  script = "read_file(\"/doesntexist\")";
+  expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_smoke) {
+  std::string data;
+  for (unsigned char i = 0; i < 128; i++) {
+    data += std::string(4096, i);
+  }
+  // Appends an additional block for verity data.
+  data += std::string(4096, 0);
+  ASSERT_EQ(129 * 4096, data.size());
+  ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+  std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+  std::string expected_root_hash =
+      "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+  // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+  std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+                                   expected_root_hash };
+  std::string hash_tree_command = android::base::Join(tokens, " ");
+
+  std::vector<std::string> transfer_list{
+    "4", "2", "0", "2", hash_tree_command,
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, "\n") },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  std::string updated;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
+  ASSERT_EQ(129 * 4096, updated.size());
+  ASSERT_EQ(data.substr(0, 128 * 4096), updated.substr(0, 128 * 4096));
+
+  // Computes the SHA256 of the salt + hash_tree_data and expects the result to match with the
+  // root_hash.
+  std::vector<unsigned char> salt_bytes;
+  ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes));
+  std::vector<unsigned char> hash_tree = std::move(salt_bytes);
+  hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end());
+
+  std::vector<unsigned char> digest(SHA256_DIGEST_LENGTH);
+  SHA256(hash_tree.data(), hash_tree.size(), digest.data());
+  ASSERT_EQ(expected_root_hash, HashTreeBuilder::BytesArrayToString(digest));
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_root_mismatch) {
+  std::string data;
+  for (size_t i = 0; i < 128; i++) {
+    data += std::string(4096, i);
+  }
+  // Appends an additional block for verity data.
+  data += std::string(4096, 0);
+  ASSERT_EQ(129 * 4096, data.size());
+  // Corrupts one bit
+  data[4096] = 'A';
+  ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+  std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+  std::string expected_root_hash =
+      "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+  // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+  std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+                                   expected_root_hash };
+  std::string hash_tree_command = android::base::Join(tokens, " ");
+
+  std::vector<std::string> transfer_list{
+    "4", "2", "0", "2", hash_tree_command,
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, "\n") },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "", kHashTreeComputationFailure);
+}
+
 TEST_F(UpdaterTest, write_value) {
   // write_value() expects two arguments.
   expect(nullptr, "write_value()", kArgsParsingFailure);
@@ -321,7 +474,7 @@
   TemporaryFile temp_file;
   std::string value = "magicvalue";
   std::string script("write_value(\"" + value + "\", \"" + std::string(temp_file.path) + "\")");
-  expect("t", script.c_str(), kNoCause);
+  expect("t", script, kNoCause);
 
   // Verify the content.
   std::string content;
@@ -330,7 +483,7 @@
 
   // Allow writing empty string.
   script = "write_value(\"\", \"" + std::string(temp_file.path) + "\")";
-  expect("t", script.c_str(), kNoCause);
+  expect("t", script, kNoCause);
 
   // Verify the content.
   ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &content));
@@ -338,7 +491,7 @@
 
   // It should fail gracefully when write fails.
   script = "write_value(\"value\", \"/proc/0/file1\")";
-  expect("", script.c_str(), kNoCause);
+  expect("", script, kNoCause);
 }
 
 TEST_F(UpdaterTest, get_stage) {
@@ -357,11 +510,11 @@
 
   // Can read the stage value.
   std::string script("get_stage(\"" + temp_file + "\")");
-  expect("2/3", script.c_str(), kNoCause);
+  expect("2/3", script, kNoCause);
 
   // Bad BCB path.
   script = "get_stage(\"doesntexist\")";
-  expect("", script.c_str(), kNoCause);
+  expect("", script, kNoCause);
 }
 
 TEST_F(UpdaterTest, set_stage) {
@@ -381,7 +534,7 @@
 
   // Write with set_stage().
   std::string script("set_stage(\"" + temp_file + "\", \"1/3\")");
-  expect(tf.path, script.c_str(), kNoCause);
+  expect(tf.path, script, kNoCause);
 
   // Verify.
   bootloader_message boot_verify;
@@ -393,10 +546,10 @@
 
   // Bad BCB path.
   script = "set_stage(\"doesntexist\", \"1/3\")";
-  expect("", script.c_str(), kNoCause);
+  expect("", script, kNoCause);
 
   script = "set_stage(\"/dev/full\", \"1/3\")";
-  expect("", script.c_str(), kNoCause);
+  expect("", script, kNoCause);
 }
 
 TEST_F(UpdaterTest, set_progress) {
@@ -448,22 +601,42 @@
   ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
 }
 
+TEST_F(UpdaterTest, block_image_update_parsing_error) {
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    // clang-format on
+  };
+
+  PackageEntries entries{
+    { "new_data", "" },
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+
+  RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure);
+}
+
 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));
+  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));
 
   // 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 = {
+  std::vector<std::string> transfer_list{
+    // clang-format off
     "4",
     "2",
     "0",
@@ -472,183 +645,108 @@
     android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,2 2 - %s:2,0,2", patch_content.size(),
                                 src_hash.c_str(), tgt_hash.c_str(), src_hash.c_str()),
     "free " + src_hash,
+    // clang-format on
   };
 
-  std::unordered_map<std::string, std::string> entries = {
+  PackageEntries entries{
     { "new_data", "" },
     { "patch_data", patch_content },
     { "transfer_list", android::base::Join(transfer_list, '\n') },
   };
 
-  // Build the update package.
-  TemporaryFile zip_file;
-  BuildUpdatePackage(entries, zip_file.release());
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
 
-  MemMapping map;
-  ASSERT_TRUE(map.MapFile(zip_file.path));
-  ZipArchiveHandle handle;
-  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+  RunBlockImageUpdate(false, entries, image_file_, "t");
 
-  // 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;
-
-  // 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) +
-      R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
-  expect("t", script.c_str(), kNoCause, &updater_info);
   // The update_file should be patched correctly.
   std::string updated_content;
-  ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
-  ASSERT_EQ(tgt_hash, get_sha1(updated_content));
-
-  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
-  CloseArchive(handle);
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content));
+  ASSERT_EQ(tgt_content, updated_content);
 }
 
 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", "2", "0", "2", "stash " + src_hash + " 2,0,2", "free " + src_hash, "fail",
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + src_hash + " 2,0,2",
+    "free " + src_hash,
+    "abort",
+    // clang-format on
   };
 
   // Add a new data of 10 bytes to test the deadlock.
-  std::unordered_map<std::string, std::string> entries = {
+  PackageEntries 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());
+  ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
 
-  MemMapping map;
-  ASSERT_TRUE(map.MapFile(zip_file.path));
-  ZipArchiveHandle handle;
-  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+  RunBlockImageUpdate(false, entries, image_file_, "");
 
-  // 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;
-  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 name_digest = get_sha1(image_file_);
   std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest;
   ASSERT_EQ(0, access(stash_base.c_str(), F_OK));
+  // Expect the stashed blocks to be freed.
   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 = {
+  std::vector<std::string> transfer_list{
+    // clang-format off
     "4",
     "1",
     "0",
     "0",
     "new 2,0,1",
+    // clang-format on
   };
 
-  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') },
+  // Write 4096 + 100 bytes of new data.
+  PackageEntries entries{
+    { "new_data", std::string(4196, 0) },
     { "patch_data", "" },
     { "transfer_list", android::base::Join(transfer_list, '\n') },
   };
 
-  TemporaryFile zip_file;
-  BuildUpdatePackage(entries, zip_file.release());
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+}
 
-  MemMapping map;
-  ASSERT_TRUE(map.MapFile(zip_file.path));
-  ZipArchiveHandle handle;
-  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+TEST_F(UpdaterTest, new_data_short_write) {
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    "1",
+    "0",
+    "0",
+    "new 2,0,1",
+    // clang-format on
+  };
 
-  // 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;
+  PackageEntries entries{
+    { "patch_data", "" },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
 
   // Updater should report the failure gracefully rather than stuck in deadlock.
-  TemporaryFile update_file;
-  std::string script_empty_data = "block_image_update(\"" + std::string(update_file.path) +
-      R"(", package_extract_file("transfer_list"), "empty_new_data", "patch_data"))";
-  expect("", script_empty_data.c_str(), kNoCause, &updater_info);
+  entries["new_data"] = "";
+  RunBlockImageUpdate(false, entries, image_file_, "");
 
-  std::string script_short_data = "block_image_update(\"" + std::string(update_file.path) +
-      R"(", package_extract_file("transfer_list"), "short_new_data", "patch_data"))";
-  expect("", script_short_data.c_str(), kNoCause, &updater_info);
+  entries["new_data"] = std::string(10, 'a');
+  RunBlockImageUpdate(false, entries, image_file_, "");
 
   // Expect to write 1 block of new data successfully.
-  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);
+  entries["new_data"] = std::string(4096, 'a');
+  RunBlockImageUpdate(false, entries, image_file_, "t");
 }
 
 TEST_F(UpdaterTest, brotli_new_data) {
@@ -681,55 +779,30 @@
     "new 2,99,100",
   };
 
-  std::unordered_map<std::string, std::string> entries = {
-    { "new.dat.br", std::move(encoded_data) },
+  PackageEntries entries{
+    { "new_data.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));
-  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(), "wb");
-  updater_info.package_zip_addr = map.addr;
-  updater_info.package_zip_len = map.length;
-
-  // Check if we can decompress the new data correctly.
-  TemporaryFile update_file;
-  std::string script_new_data =
-      "block_image_update(\"" + std::string(update_file.path) +
-      R"(", package_extract_file("transfer_list"), "new.dat.br", "patch_data"))";
-  expect("t", script_new_data.c_str(), kNoCause, &updater_info);
+  RunBlockImageUpdate(false, entries, image_file_, "t");
 
   std::string updated_content;
-  ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &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(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(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 = {
+  std::vector<std::string> transfer_list_fail{
+    // clang-format off
     "4",
     "2",
     "0",
@@ -737,11 +810,13 @@
     "stash " + block1_hash + " 2,0,1",
     "move " + block1_hash + " 2,1,2 1 2,0,1",
     "stash " + block3_hash + " 2,2,3",
-    "fail",
+    "abort",
+    // clang-format on
   };
 
   // Mimic a resumed update with the same transfer commands.
-  std::vector<std::string> transfer_list_continue = {
+  std::vector<std::string> transfer_list_continue{
+    // clang-format off
     "4",
     "2",
     "0",
@@ -750,127 +825,88 @@
     "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",
+    // clang-format on
   };
 
-  std::unordered_map<std::string, std::string> entries = {
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+
+  PackageEntries 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') },
+    { "transfer_list", android::base::Join(transfer_list_fail, '\n') },
   };
 
-  // Build the update package.
-  TemporaryFile zip_file;
-  BuildUpdatePackage(entries, zip_file.release());
+  // "2\nstash " + block3_hash + " 2,2,3"
+  std::string last_command_content =
+      "2\n" + transfer_list_fail[TransferList::kTransferListHeaderLines + 2];
 
-  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);
+  RunBlockImageUpdate(false, entries, image_file_, "");
 
   // 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 last_command_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+  EXPECT_EQ(last_command_content, last_command_actual);
+
   std::string updated_contents;
-  ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents));
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &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);
+  // "Resume" the update. Expect the first 'move' to be skipped but the second 'move' to be
+  // executed. Note that we intentionally reset the image file.
+  entries["transfer_list"] = android::base::Join(transfer_list_continue, '\n');
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+  RunBlockImageUpdate(false, entries, image_file_, "t");
 
-  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
-  CloseArchive(handle);
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents));
+  ASSERT_EQ(block1 + block2 + block1, updated_contents);
 }
 
 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(4096, '1');
+  std::string block2(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::vector<std::string> transfer_list_unresumable{
+    // clang-format off
+    "4",
+    "2",
+    "0",
+    "2",
+    "stash " + block1_hash + " 2,0,1",
+    "move " + block2_hash + " 2,1,2 1 2,0,1",
+    // clang-format on
   };
 
-  std::unordered_map<std::string, std::string> entries = {
+  PackageEntries entries{
     { "new_data", "" },
     { "patch_data", "" },
-    { "transfer_list_unresumable", android::base::Join(transfer_list_unresumable, '\n') },
+    { "transfer_list", android::base::Join(transfer_list_unresumable, '\n') },
   };
 
-  // Build the update package.
-  TemporaryFile zip_file;
-  BuildUpdatePackage(entries, zip_file.release());
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1, image_file_));
 
-  MemMapping map;
-  ASSERT_TRUE(map.MapFile(zip_file.path));
-  ZipArchiveHandle handle;
-  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+  std::string last_command_content =
+      "0\n" + transfer_list_unresumable[TransferList::kTransferListHeaderLines];
+  ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
 
-  // 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;
+  RunBlockImageUpdate(false, entries, image_file_, "");
 
-  // 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);
+  // The last_command_file will be deleted if the update encounters an unresumable failure later.
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
 }
 
 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(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(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 = {
+  std::vector<std::string> transfer_list_verify{
+    // clang-format off
     "4",
     "2",
     "0",
@@ -879,55 +915,265 @@
     "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",
+    // clang-format on
   };
 
-  std::unordered_map<std::string, std::string> entries = {
+  PackageEntries entries{
     { "new_data", "" },
     { "patch_data", "" },
-    { "transfer_list_verify", android::base::Join(transfer_list_verify, '\n') },
+    { "transfer_list", android::base::Join(transfer_list_verify, '\n') },
   };
 
-  // Build the update package.
-  TemporaryFile zip_file;
-  BuildUpdatePackage(entries, zip_file.release());
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1 + block3, image_file_));
 
-  MemMapping map;
-  ASSERT_TRUE(map.MapFile(zip_file.path));
-  ZipArchiveHandle handle;
-  ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+  // Last command: "move " + block1_hash + " 2,1,2 1 2,0,1"
+  std::string last_command_content =
+      "2\n" + transfer_list_verify[TransferList::kTransferListHeaderLines + 2];
 
-  // 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;
+  // First run: expect the verification to succeed and the last_command_file is intact.
+  ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
 
-  std::string src_content = block1 + block1 + block3;
-  TemporaryFile update_file;
-  ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+  RunBlockImageUpdate(true, entries, image_file_, "t");
 
-  ASSERT_TRUE(
-      android::base::WriteStringToFile("2\nstash " + block3_hash + " 2,2,3", last_command_file));
+  std::string last_command_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+  EXPECT_EQ(last_command_content, last_command_actual);
 
-  // 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);
+  // Second run with a mismatching block image: expect the verification to succeed but
+  // last_command_file to be deleted; because the target blocks in the last command don't have the
+  // expected contents for the second move command.
+  ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+  RunBlockImageUpdate(true, entries, image_file_, "t");
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+}
 
-  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);
+class ResumableUpdaterTest : public testing::TestWithParam<size_t> {
+ protected:
+  void SetUp() override {
+    RegisterBuiltins();
+    RegisterInstallFunctions();
+    RegisterBlockImageFunctions();
 
-  // 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));
+    Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+    Paths::Get().set_last_command_file(temp_last_command_.path);
+    Paths::Get().set_stash_directory_base(temp_stash_base_.path);
 
-  ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
-  CloseArchive(handle);
+    // Enable a special command "abort" to simulate interruption.
+    Command::abort_allowed_ = true;
+
+    index_ = GetParam();
+    image_file_ = image_temp_file_.path;
+    last_command_file_ = temp_last_command_.path;
+  }
+
+  void TearDown() override {
+    // Clean up the last_command_file if any.
+    ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+    // Clear partition updated marker if any.
+    std::string updated_marker{ temp_stash_base_.path };
+    updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+    ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
+  }
+
+  TemporaryFile temp_saved_source_;
+  TemporaryDir temp_stash_base_;
+  std::string last_command_file_;
+  std::string image_file_;
+  size_t index_;
+
+ private:
+  TemporaryFile temp_last_command_;
+  TemporaryFile image_temp_file_;
+};
+
+static std::string g_source_image;
+static std::string g_target_image;
+static PackageEntries g_entries;
+
+static std::vector<std::string> GenerateTransferList() {
+  std::string a(4096, 'a');
+  std::string b(4096, 'b');
+  std::string c(4096, 'c');
+  std::string d(4096, 'd');
+  std::string e(4096, 'e');
+  std::string f(4096, 'f');
+  std::string g(4096, 'g');
+  std::string h(4096, 'h');
+  std::string i(4096, 'i');
+  std::string zero(4096, '\0');
+
+  std::string a_hash = get_sha1(a);
+  std::string b_hash = get_sha1(b);
+  std::string c_hash = get_sha1(c);
+  std::string e_hash = get_sha1(e);
+
+  auto loc = [](const std::string& range_text) {
+    std::vector<std::string> pieces = android::base::Split(range_text, "-");
+    size_t left;
+    size_t right;
+    if (pieces.size() == 1) {
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      right = left + 1;
+    } else {
+      CHECK_EQ(2u, pieces.size());
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      CHECK(android::base::ParseUint(pieces[1], &right));
+      right++;
+    }
+    return android::base::StringPrintf("2,%zu,%zu", left, right);
+  };
+
+  // patch 1: "b d c" -> "g"
+  TemporaryFile patch_file_bdc_g;
+  std::string bdc = b + d + c;
+  std::string bdc_hash = get_sha1(bdc);
+  std::string g_hash = get_sha1(g);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(),
+                             reinterpret_cast<const uint8_t*>(g.data()), g.size(),
+                             patch_file_bdc_g.path, nullptr));
+  std::string patch_bdc_g;
+  CHECK(android::base::ReadFileToString(patch_file_bdc_g.path, &patch_bdc_g));
+
+  // patch 2: "a b c d" -> "d c b"
+  TemporaryFile patch_file_abcd_dcb;
+  std::string abcd = a + b + c + d;
+  std::string abcd_hash = get_sha1(abcd);
+  std::string dcb = d + c + b;
+  std::string dcb_hash = get_sha1(dcb);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(),
+                             reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(),
+                             patch_file_abcd_dcb.path, nullptr));
+  std::string patch_abcd_dcb;
+  CHECK(android::base::ReadFileToString(patch_file_abcd_dcb.path, &patch_abcd_dcb));
+
+  std::vector<std::string> transfer_list{
+    "4",
+    "10",  // total blocks written
+    "2",   // maximum stash entries
+    "2",   // maximum number of stashed blocks
+
+    // a b c d e a b c d e
+    "stash " + b_hash + " " + loc("1"),
+    // a b c d e a b c d e    [b(1)]
+    "stash " + c_hash + " " + loc("2"),
+    // a b c d e a b c d e    [b(1)][c(2)]
+    "new " + loc("1-2"),
+    // a i h d e a b c d e    [b(1)][c(2)]
+    "zero " + loc("0"),
+    // 0 i h d e a b c d e    [b(1)][c(2)]
+
+    // bsdiff "b d c" (from stash, 3, stash) to get g(3)
+    android::base::StringPrintf(
+        "bsdiff 0 %zu %s %s %s 3 %s %s %s:%s %s:%s",
+        patch_bdc_g.size(),                  // patch start (0), patch length
+        bdc_hash.c_str(),                    // source hash
+        g_hash.c_str(),                      // target hash
+        loc("3").c_str(),                    // target range
+        loc("3").c_str(), loc("1").c_str(),  // load "d" from block 3, into buffer at offset 1
+        b_hash.c_str(), loc("0").c_str(),    // load "b" from stash, into buffer at offset 0
+        c_hash.c_str(), loc("2").c_str()),   // load "c" from stash, into buffer at offset 2
+
+    // 0 i h g e a b c d e    [b(1)][c(2)]
+    "free " + b_hash,
+    // 0 i h g e a b c d e    [c(2)]
+    "free " + a_hash,
+    // 0 i h g e a b c d e
+    "stash " + a_hash + " " + loc("5"),
+    // 0 i h g e a b c d e    [a(5)]
+    "move " + e_hash + " " + loc("5") + " 1 " + loc("4"),
+    // 0 i h g e e b c d e    [a(5)]
+
+    // bsdiff "a b c d" (from stash, 6-8) to "d c b" (6-8)
+    android::base::StringPrintf(  //
+        "bsdiff %zu %zu %s %s %s 4 %s %s %s:%s",
+        patch_bdc_g.size(),                          // patch start
+        patch_bdc_g.size() + patch_abcd_dcb.size(),  // patch length
+        abcd_hash.c_str(),                           // source hash
+        dcb_hash.c_str(),                            // target hash
+        loc("6-8").c_str(),                          // target range
+        loc("6-8").c_str(),                          // load "b c d" from blocks 6-8
+        loc("1-3").c_str(),                          //   into buffer at offset 1-3
+        a_hash.c_str(),                              // load "a" from stash
+        loc("0").c_str()),                           //   into buffer at offset 0
+
+    // 0 i h g e e d c b e    [a(5)]
+    "new " + loc("4"),
+    // 0 i h g f e d c b e    [a(5)]
+    "move " + a_hash + " " + loc("9") + " 1 - " + a_hash + ":" + loc("0"),
+    // 0 i h g f e d c b a    [a(5)]
+    "free " + a_hash,
+    // 0 i h g f e d c b a
+  };
+
+  std::string new_data = i + h + f;
+  std::string patch_data = patch_bdc_g + patch_abcd_dcb;
+
+  g_entries = {
+    { "new_data", new_data },
+    { "patch_data", patch_data },
+  };
+  g_source_image = a + b + c + d + e + a + b + c + d + e;
+  g_target_image = zero + i + h + g + f + e + d + c + b + a;
+
+  return transfer_list;
+}
+
+static const std::vector<std::string> g_transfer_list = GenerateTransferList();
+
+INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest,
+                        ::testing::Range(static_cast<size_t>(0),
+                                         g_transfer_list.size() -
+                                             TransferList::kTransferListHeaderLines));
+
+TEST_P(ResumableUpdaterTest, InterruptVerifyResume) {
+  ASSERT_TRUE(android::base::WriteStringToFile(g_source_image, image_file_));
+
+  LOG(INFO) << "Interrupting at line " << index_ << " ("
+            << g_transfer_list[TransferList::kTransferListHeaderLines + index_] << ")";
+
+  std::vector<std::string> transfer_list_copy{ g_transfer_list };
+  transfer_list_copy[TransferList::kTransferListHeaderLines + index_] = "abort";
+
+  g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n');
+
+  // Run update that's expected to fail.
+  RunBlockImageUpdate(false, g_entries, image_file_, "");
+
+  std::string last_command_expected;
+
+  // Assert the last_command_file.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    last_command_expected = std::to_string(index_ - 1) + "\n" +
+                            g_transfer_list[TransferList::kTransferListHeaderLines + index_ - 1];
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n');
+
+  // Resume the interrupted update, by doing verification first.
+  RunBlockImageUpdate(true, g_entries, image_file_, "t");
+
+  // last_command_file should remain intact.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  // Resume the update.
+  RunBlockImageUpdate(false, g_entries, image_file_, "t");
+
+  // last_command_file should be gone after successful update.
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+
+  std::string updated_image_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual));
+  ASSERT_EQ(g_target_image, updated_image_actual);
 }
diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp
index 2ef3828..3246ecd 100644
--- a/tests/component/verifier_test.cpp
+++ b/tests/component/verifier_test.cpp
@@ -16,7 +16,6 @@
 
 #include <errno.h>
 #include <fcntl.h>
-#include <gtest/gtest.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/stat.h>
@@ -28,9 +27,10 @@
 #include <android-base/file.h>
 #include <android-base/stringprintf.h>
 #include <android-base/test_utils.h>
+#include <gtest/gtest.h>
 
 #include "common/test_constants.h"
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
 #include "verifier.h"
 
 using namespace std::string_literals;
diff --git a/tests/manual/recovery_test.cpp b/tests/manual/recovery_test.cpp
index 64e3b59..e1d0771 100644
--- a/tests/manual/recovery_test.cpp
+++ b/tests/manual/recovery_test.cpp
@@ -14,27 +14,22 @@
  * limitations under the License.
  */
 
-#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
 #include <string.h>
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <memory>
 #include <string>
-#include <vector>
 
 #include <android-base/file.h>
-#include <android-base/strings.h>
 #include <android/log.h>
 #include <gtest/gtest.h>
-#include <png.h>
 #include <private/android_logger.h>
 
-#include "minui/minui.h"
-
-static const std::string myFilename = "/data/misc/recovery/inject.txt";
-static const std::string myContent = "Hello World\nWelcome to my recovery\n";
-static const std::string kLocale = "zu";
-static const std::string kResourceTestDir = "/data/nativetest/recovery/";
+static const std::string kInjectTxtFilename = "/data/misc/recovery/inject.txt";
+static const std::string kInjectTxtContent = "Hello World\nWelcome to my recovery\n";
 
 // Failure is expected on systems that do not deliver either the
 // recovery-persist or recovery-refresh executables. Tests also require
@@ -44,9 +39,9 @@
                          const char *buf, size_t len, void *arg) {
   EXPECT_EQ(LOG_ID_SYSTEM, logId);
   EXPECT_EQ(ANDROID_LOG_INFO, prio);
-  EXPECT_NE(std::string::npos, myFilename.find(filename));
-  EXPECT_EQ(myContent, buf);
-  EXPECT_EQ(myContent.size(), len);
+  EXPECT_NE(std::string::npos, kInjectTxtFilename.find(filename));
+  EXPECT_EQ(kInjectTxtContent, buf);
+  EXPECT_EQ(kInjectTxtContent.size(), len);
   EXPECT_EQ(nullptr, arg);
   return len;
 }
@@ -59,13 +54,14 @@
   ssize_t ret = __android_log_pmsg_file_read(
       LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr);
   if (ret == -ENOENT) {
-    EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
-        myFilename.c_str(), myContent.c_str(), myContent.size()));
+    EXPECT_LT(0, __android_log_pmsg_file_write(
+                     LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(),
+                     kInjectTxtContent.c_str(), kInjectTxtContent.size()));
 
-    fprintf(stderr, "injected test data, requires two intervening reboots "
-        "to check for replication\n");
+    fprintf(stderr,
+            "injected test data, requires two intervening reboots to check for replication\n");
   }
-  EXPECT_EQ(static_cast<ssize_t>(myContent.size()), ret);
+  EXPECT_EQ(static_cast<ssize_t>(kInjectTxtContent.size()), ret);
 }
 
 // recovery.persist - Requires recovery.inject, then a reboot, then
@@ -76,149 +72,18 @@
   ssize_t ret = __android_log_pmsg_file_read(
       LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr);
   if (ret == -ENOENT) {
-    EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
-        myFilename.c_str(), myContent.c_str(), myContent.size()));
+    EXPECT_LT(0, __android_log_pmsg_file_write(
+                     LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(),
+                     kInjectTxtContent.c_str(), kInjectTxtContent.size()));
 
-    fprintf(stderr, "injected test data, requires intervening reboot "
-        "to check for storage\n");
+    fprintf(stderr, "injected test data, requires intervening reboot to check for storage\n");
   }
 
   std::string buf;
-  EXPECT_TRUE(android::base::ReadFileToString(myFilename, &buf));
-  EXPECT_EQ(myContent, buf);
-  if (access(myFilename.c_str(), F_OK) == 0) {
-    fprintf(stderr, "Removing persistent test data, "
-        "check if reconstructed on reboot\n");
+  EXPECT_TRUE(android::base::ReadFileToString(kInjectTxtFilename, &buf));
+  EXPECT_EQ(kInjectTxtContent, buf);
+  if (access(kInjectTxtFilename.c_str(), F_OK) == 0) {
+    fprintf(stderr, "Removing persistent test data, check if reconstructed on reboot\n");
   }
-  EXPECT_EQ(0, unlink(myFilename.c_str()));
+  EXPECT_EQ(0, unlink(kInjectTxtFilename.c_str()));
 }
-
-std::vector<std::string> image_dir {
-  "res-mdpi/images/",
-  "res-hdpi/images/",
-  "res-xhdpi/images/",
-  "res-xxhdpi/images/",
-  "res-xxxhdpi/images/"
-};
-
-static int png_filter(const dirent* de) {
-  if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) {
-    return 0;
-  }
-  return 1;
-}
-
-// Find out all png files to test under /data/nativetest/recovery/.
-static std::vector<std::string> add_files() {
-  std::vector<std::string> files;
-  for (const std::string& str : image_dir) {
-    std::string dir_path = kResourceTestDir + str;
-    dirent** namelist;
-    int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort);
-    if (n == -1) {
-      printf("Failed to scan dir %s: %s\n", kResourceTestDir.c_str(), strerror(errno));
-      return files;
-    }
-    if (n == 0) {
-      printf("No file is added for test in %s\n", kResourceTestDir.c_str());
-    }
-
-    while (n--) {
-      std::string file_path = dir_path + namelist[n]->d_name;
-      files.push_back(file_path);
-      free(namelist[n]);
-    }
-    free(namelist);
-  }
-  return files;
-}
-
-class ResourceTest : public testing::TestWithParam<std::string> {
- public:
-  static std::vector<std::string> png_list;
-
-  // Parse a png file and test if it's qualified for the background text image
-  // under recovery.
-  void SetUp() override {
-    std::string file_path = GetParam();
-    fp = fopen(file_path.c_str(), "rbe");
-    ASSERT_NE(nullptr, fp);
-
-    unsigned char header[8];
-    size_t bytesRead = fread(header, 1, sizeof(header), fp);
-    ASSERT_EQ(sizeof(header), bytesRead);
-    ASSERT_EQ(0, png_sig_cmp(header, 0, sizeof(header)));
-
-    png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
-    ASSERT_NE(nullptr, png_ptr);
-
-    info_ptr = png_create_info_struct(png_ptr);
-    ASSERT_NE(nullptr, info_ptr);
-
-    png_init_io(png_ptr, fp);
-    png_set_sig_bytes(png_ptr, sizeof(header));
-    png_read_info(png_ptr, info_ptr);
-
-    int color_type, bit_depth;
-    png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, nullptr, nullptr,
-                 nullptr);
-    ASSERT_EQ(PNG_COLOR_TYPE_GRAY, color_type) << "Recovery expects grayscale PNG file.";
-    ASSERT_LT(static_cast<png_uint_32>(5), width);
-    ASSERT_LT(static_cast<png_uint_32>(0), height);
-    if (bit_depth <= 8) {
-      // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
-      png_set_expand_gray_1_2_4_to_8(png_ptr);
-    }
-
-    png_byte channels = png_get_channels(png_ptr, info_ptr);
-    ASSERT_EQ(1, channels) << "Recovery background text images expects 1-channel PNG file.";
-  }
-
-  void TearDown() override {
-    if (png_ptr != nullptr && info_ptr != nullptr) {
-      png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
-    }
-
-    if (fp != nullptr) {
-      fclose(fp);
-    }
-  }
-
- protected:
-  png_structp png_ptr;
-  png_infop info_ptr;
-  png_uint_32 width, height;
-
-  FILE* fp;
-};
-
-std::vector<std::string> ResourceTest::png_list = add_files();
-
-TEST_P(ResourceTest, ValidateLocale) {
-  std::vector<unsigned char> row(width);
-  for (png_uint_32 y = 0; y < height; ++y) {
-    png_read_row(png_ptr, row.data(), nullptr);
-    int w = (row[1] << 8) | row[0];
-    int h = (row[3] << 8) | row[2];
-    int len = row[4];
-    EXPECT_LT(0, w);
-    EXPECT_LT(0, h);
-    EXPECT_LT(0, len) << "Locale string should be non-empty.";
-    EXPECT_NE(0, row[5]) << "Locale string is missing.";
-
-    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));
-      break;
-    } else {
-      for (int i = 0; i < h; ++i, ++y) {
-        png_read_row(png_ptr, row.data(), nullptr);
-      }
-    }
-  }
-}
-
-INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourceTest,
-                        ::testing::ValuesIn(ResourceTest::png_list.cbegin(),
-                                            ResourceTest::png_list.cend()));
diff --git a/tests/testdata/font.png b/tests/testdata/font.png
new file mode 100644
index 0000000..d95408a
--- /dev/null
+++ b/tests/testdata/font.png
Binary files differ
diff --git a/tests/testdata/gzipped_source b/tests/testdata/gzipped_source
new file mode 100644
index 0000000..6d425d0
--- /dev/null
+++ b/tests/testdata/gzipped_source
Binary files differ
diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target
new file mode 100644
index 0000000..5621262
--- /dev/null
+++ b/tests/testdata/gzipped_target
Binary files differ
diff --git a/tests/testdata/loop00000.png b/tests/testdata/loop00000.png
new file mode 100644
index 0000000..0e11c01
--- /dev/null
+++ b/tests/testdata/loop00000.png
Binary files differ
diff --git a/tests/testdata/new.file b/tests/testdata/new.file
deleted file mode 100644
index cdeb8fd..0000000
--- a/tests/testdata/new.file
+++ /dev/null
Binary files differ
diff --git a/tests/testdata/old.file b/tests/testdata/old.file
deleted file mode 100644
index 166c873..0000000
--- a/tests/testdata/old.file
+++ /dev/null
Binary files differ
diff --git a/tests/unit/applypatch_test.cpp b/tests/unit/applypatch_test.cpp
new file mode 100644
index 0000000..066f981
--- /dev/null
+++ b/tests/unit/applypatch_test.cpp
@@ -0,0 +1,291 @@
+/*
+ * 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 agree to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/test_utils.h>
+#include <android-base/unique_fd.h>
+#include <gtest/gtest.h>
+
+#include "applypatch/applypatch.h"
+#include "common/test_constants.h"
+#include "edify/expr.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+
+using namespace std::string_literals;
+
+class ApplyPatchTest : public ::testing::Test {
+ protected:
+  void SetUp() override {
+    source_file = from_testdata_base("boot.img");
+    FileContents boot_fc;
+    ASSERT_TRUE(LoadFileContents(source_file, &boot_fc));
+    source_size = boot_fc.data.size();
+    source_sha1 = print_sha1(boot_fc.sha1);
+
+    target_file = from_testdata_base("recovery.img");
+    FileContents recovery_fc;
+    ASSERT_TRUE(LoadFileContents(target_file, &recovery_fc));
+    target_size = recovery_fc.data.size();
+    target_sha1 = print_sha1(recovery_fc.sha1);
+
+    source_partition = Partition(source_file, source_size, source_sha1);
+    target_partition = Partition(partition_file.path, target_size, target_sha1);
+
+    srand(time(nullptr));
+    bad_sha1_a = android::base::StringPrintf("%040x", rand());
+    bad_sha1_b = android::base::StringPrintf("%040x", rand());
+
+    // Reset the cache backup file.
+    Paths::Get().set_cache_temp_source(cache_temp_source.path);
+  }
+
+  void TearDown() override {
+    ASSERT_TRUE(android::base::RemoveFileIfExists(cache_temp_source.path));
+  }
+
+  std::string source_file;
+  std::string source_sha1;
+  size_t source_size;
+
+  std::string target_file;
+  std::string target_sha1;
+  size_t target_size;
+
+  std::string bad_sha1_a;
+  std::string bad_sha1_b;
+
+  Partition source_partition;
+  Partition target_partition;
+
+ private:
+  TemporaryFile partition_file;
+  TemporaryFile cache_temp_source;
+};
+
+TEST_F(ApplyPatchTest, CheckPartition) {
+  ASSERT_TRUE(CheckPartition(source_partition));
+}
+
+TEST_F(ApplyPatchTest, CheckPartition_Mismatching) {
+  ASSERT_FALSE(CheckPartition(Partition(source_file, target_size, target_sha1)));
+  ASSERT_FALSE(CheckPartition(Partition(source_file, source_size, bad_sha1_a)));
+
+  ASSERT_FALSE(CheckPartition(Partition(source_file, source_size - 1, source_sha1)));
+  ASSERT_FALSE(CheckPartition(Partition(source_file, source_size + 1, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck) {
+  ASSERT_TRUE(PatchPartitionCheck(target_partition, source_partition));
+
+  ASSERT_TRUE(
+      PatchPartitionCheck(Partition(source_file, source_size - 1, source_sha1), source_partition));
+
+  ASSERT_TRUE(
+      PatchPartitionCheck(Partition(source_file, source_size + 1, source_sha1), source_partition));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup) {
+  ASSERT_FALSE(
+      PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+  Paths::Get().set_cache_temp_source(source_file);
+  ASSERT_TRUE(
+      PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup_BothCorrupted) {
+  ASSERT_FALSE(
+      PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+  Paths::Get().set_cache_temp_source(target_file);
+  ASSERT_FALSE(
+      PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartition) {
+  FileContents patch_fc;
+  ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot.p"), &patch_fc));
+  Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+  FileContents bonus_fc;
+  ASSERT_TRUE(LoadFileContents(from_testdata_base("bonus.file"), &bonus_fc));
+  Value bonus(Value::Type::BLOB, std::string(bonus_fc.data.cbegin(), bonus_fc.data.cend()));
+
+  ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, &bonus));
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchTest, PatchPartitionWithoutBonusFile) {
+  FileContents patch_fc;
+  ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot-with-bonus.p"), &patch_fc));
+  Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+  ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, nullptr));
+}
+
+class FreeCacheTest : public ::testing::Test {
+ protected:
+  static constexpr size_t PARTITION_SIZE = 4096 * 10;
+
+  // Returns a sorted list of files in |dirname|.
+  static std::vector<std::string> FindFilesInDir(const std::string& dirname) {
+    std::vector<std::string> file_list;
+
+    std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir);
+    struct dirent* de;
+    while ((de = readdir(d.get())) != 0) {
+      std::string path = dirname + "/" + de->d_name;
+
+      struct stat st;
+      if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+        file_list.emplace_back(de->d_name);
+      }
+    }
+
+    std::sort(file_list.begin(), file_list.end());
+    return file_list;
+  }
+
+  void AddFilesToDir(const std::string& dir, const std::vector<std::string>& files) {
+    std::string zeros(4096, 0);
+    for (const auto& file : files) {
+      temporary_files_.push_back(dir + "/" + file);
+      ASSERT_TRUE(android::base::WriteStringToFile(zeros, temporary_files_.back()));
+    }
+  }
+
+  void SetUp() override {
+    Paths::Get().set_cache_log_directory(mock_log_dir.path);
+    temporary_files_.clear();
+  }
+
+  void TearDown() override {
+    for (const auto& file : temporary_files_) {
+      ASSERT_TRUE(android::base::RemoveFileIfExists(file));
+    }
+  }
+
+  // A mock method to calculate the free space. It assumes the partition has a total size of 40960
+  // bytes and all files are 4096 bytes in size.
+  static size_t MockFreeSpaceChecker(const std::string& dirname) {
+    std::vector<std::string> files = FindFilesInDir(dirname);
+    return PARTITION_SIZE - 4096 * files.size();
+  }
+
+  TemporaryDir mock_cache;
+  TemporaryDir mock_log_dir;
+
+ private:
+  std::vector<std::string> temporary_files_;
+};
+
+TEST_F(FreeCacheTest, FreeCacheSmoke) {
+  std::vector<std::string> files = { "file1", "file2", "file3" };
+  AddFilesToDir(mock_cache.path, files);
+  ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+  ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+  ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_cache.path, MockFreeSpaceChecker));
+
+  ASSERT_EQ(std::vector<std::string>{ "file3" }, FindFilesInDir(mock_cache.path));
+  ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheFreeSpaceCheckerError) {
+  std::vector<std::string> files{ "file1", "file2", "file3" };
+  AddFilesToDir(mock_cache.path, files);
+  ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+  ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+  ASSERT_FALSE(
+      RemoveFilesInDirectory(4096 * 9, mock_cache.path, [](const std::string&) { return -1; }));
+}
+
+TEST_F(FreeCacheTest, FreeCacheOpenFile) {
+  std::vector<std::string> files = { "file1", "file2" };
+  AddFilesToDir(mock_cache.path, files);
+  ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+  ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_cache.path));
+
+  std::string file1_path = mock_cache.path + "/file1"s;
+  android::base::unique_fd fd(open(file1_path.c_str(), O_RDONLY));
+
+  // file1 can't be deleted as it's opened by us.
+  ASSERT_FALSE(RemoveFilesInDirectory(4096 * 10, mock_cache.path, MockFreeSpaceChecker));
+
+  ASSERT_EQ(std::vector<std::string>{ "file1" }, FindFilesInDir(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsSmoke) {
+  std::vector<std::string> log_files = { "last_log", "last_log.1", "last_kmsg.2", "last_log.5",
+                                         "last_log.10" };
+  AddFilesToDir(mock_log_dir.path, log_files);
+  ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+  ASSERT_TRUE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+  // Logs with a higher index will be deleted first
+  std::vector<std::string> expected = { "last_log", "last_log.1" };
+  ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+  ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsStringComparison) {
+  std::vector<std::string> log_files = { "last_log.1", "last_kmsg.1", "last_log.not_number",
+                                         "last_kmsgrandom" };
+  AddFilesToDir(mock_log_dir.path, log_files);
+  ASSERT_EQ(4096 * 6, MockFreeSpaceChecker(mock_log_dir.path));
+
+  ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_log_dir.path, MockFreeSpaceChecker));
+
+  // Logs with incorrect format will be deleted first; and the last_kmsg with the same index is
+  // deleted before last_log.
+  std::vector<std::string> expected = { "last_log.1" };
+  ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+  ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsOtherFiles) {
+  std::vector<std::string> log_files = { "last_install", "command", "block.map", "last_log",
+                                         "last_kmsg.1" };
+  AddFilesToDir(mock_log_dir.path, log_files);
+  ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+  ASSERT_FALSE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+  // Non log files in /cache/recovery won't be deleted.
+  std::vector<std::string> expected = { "block.map", "command", "last_install" };
+  ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+}
diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp
new file mode 100644
index 0000000..8a54df7
--- /dev/null
+++ b/tests/unit/commands_test.cpp
@@ -0,0 +1,554 @@
+/*
+ * 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 <algorithm>
+#include <string>
+
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
+#include "private/commands.h"
+
+TEST(CommandsTest, ParseType) {
+  ASSERT_EQ(Command::Type::ZERO, Command::ParseType("zero"));
+  ASSERT_EQ(Command::Type::NEW, Command::ParseType("new"));
+  ASSERT_EQ(Command::Type::ERASE, Command::ParseType("erase"));
+  ASSERT_EQ(Command::Type::MOVE, Command::ParseType("move"));
+  ASSERT_EQ(Command::Type::BSDIFF, Command::ParseType("bsdiff"));
+  ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff"));
+  ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash"));
+  ASSERT_EQ(Command::Type::FREE, Command::ParseType("free"));
+  ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree"));
+}
+
+TEST(CommandsTest, ParseType_InvalidCommand) {
+  ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo"));
+  ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar"));
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly) {
+  const std::vector<std::string> tokens{
+    "4,569884,569904,591946,592043",
+    "117",
+    "4,566779,566799,591946,592043",
+  };
+  TargetInfo target;
+  SourceInfo source;
+  std::string err;
+  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+      tokens, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+      "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+  ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                       RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+            target);
+  ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                       RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), {}, {}),
+            source);
+  ASSERT_EQ(117, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly) {
+  const std::vector<std::string> tokens{
+    "2,350729,350731",
+    "2",
+    "-",
+    "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15:2,0,2",
+  };
+  TargetInfo target;
+  SourceInfo source;
+  std::string err;
+  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+      tokens, "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", &target,
+      "1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", &source, &err));
+  ASSERT_EQ(
+      TargetInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 350729, 350731 } })),
+      target);
+  ASSERT_EQ(
+      SourceInfo("1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", {}, {},
+                 {
+                     StashInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 0, 2 } })),
+                 }),
+      source);
+  ASSERT_EQ(2, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes) {
+  const std::vector<std::string> tokens{
+    "4,611641,611643,636981,637075",
+    "96",
+    "4,636981,637075,770665,770666",
+    "4,0,94,95,96",
+    "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+  };
+  TargetInfo target;
+  SourceInfo source;
+  std::string err;
+  ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+      tokens, "4734d1b241eb3d0f993714aaf7d665fae43772b6", &target,
+      "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", &source, &err));
+  ASSERT_EQ(TargetInfo("4734d1b241eb3d0f993714aaf7d665fae43772b6",
+                       RangeSet({ { 611641, 611643 }, { 636981, 637075 } })),
+            target);
+  ASSERT_EQ(SourceInfo(
+                "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+                RangeSet({ { 636981, 637075 }, { 770665, 770666 } }),  // source ranges
+                RangeSet({ { 0, 94 }, { 95, 96 } }),                   // source location
+                {
+                    StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", RangeSet({ { 94, 95 } })),
+                }),
+            source);
+  ASSERT_EQ(96, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput) {
+  const std::vector<std::string> tokens{
+    "4,611641,611643,636981,637075",
+    "96",
+    "4,636981,637075,770665,770666",
+    "4,0,94,95,96",
+    "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+  };
+  TargetInfo target;
+  SourceInfo source;
+  std::string err;
+
+  // Mismatching block count.
+  {
+    std::vector<std::string> tokens_copy(tokens);
+    tokens_copy[1] = "97";
+    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+        tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+  }
+
+  // Excess stashes (causing block count mismatch).
+  {
+    std::vector<std::string> tokens_copy(tokens);
+    tokens_copy.push_back("e145a2f83a33334714ac65e34969c1f115e54a6f:2,0,22");
+    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+        tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+  }
+
+  // Invalid args.
+  for (size_t i = 0; i < tokens.size(); i++) {
+    TargetInfo target;
+    SourceInfo source;
+    std::string err;
+    ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+        std::vector<std::string>(tokens.cbegin() + i + 1, tokens.cend()),
+        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+        "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+  }
+}
+
+TEST(CommandsTest, Parse_EmptyInput) {
+  std::string err;
+  ASSERT_FALSE(Command::Parse("", 0, &err));
+  ASSERT_EQ("invalid type", err);
+}
+
+TEST(CommandsTest, Parse_ABORT_Allowed) {
+  Command::abort_allowed_ = true;
+
+  const std::string input{ "abort" };
+  std::string err;
+  Command command = Command::Parse(input, 0, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(TargetInfo(), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ABORT_NotAllowed) {
+  const std::string input{ "abort" };
+  std::string err;
+  Command command = Command::Parse(input, 0, &err);
+  ASSERT_FALSE(command);
+}
+
+TEST(CommandsTest, Parse_BSDIFF) {
+  const std::string input{
+    "bsdiff 0 148 "
+    "f201a4e04bd3860da6ad47b957ef424d58a58f8c 9d5d223b4bc5c45dbd25a799c4f1a98466731599 "
+    "4,565704,565752,566779,566799 "
+    "68 4,64525,64545,565704,565752"
+  };
+  std::string err;
+  Command command = Command::Parse(input, 1, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::BSDIFF, command.type());
+  ASSERT_EQ(1, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("9d5d223b4bc5c45dbd25a799c4f1a98466731599",
+                       RangeSet({ { 565704, 565752 }, { 566779, 566799 } })),
+            command.target());
+  ASSERT_EQ(SourceInfo("f201a4e04bd3860da6ad47b957ef424d58a58f8c",
+                       RangeSet({ { 64525, 64545 }, { 565704, 565752 } }), RangeSet(), {}),
+            command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(0, 148), command.patch());
+}
+
+TEST(CommandsTest, Parse_ERASE) {
+  const std::string input{ "erase 2,5,10" };
+  std::string err;
+  Command command = Command::Parse(input, 2, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::ERASE, command.type());
+  ASSERT_EQ(2, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 5, 10 } })), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_FREE) {
+  const std::string input{ "free hash1" };
+  std::string err;
+  Command command = Command::Parse(input, 3, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::FREE, command.type());
+  ASSERT_EQ(3, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo(), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo("hash1", RangeSet()), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_IMGDIFF) {
+  const std::string input{
+    "imgdiff 29629269 185 "
+    "a6b1c49aed1b57a2aab1ec3e1505b945540cd8db 51978f65035f584a8ef7afa941dacb6d5e862164 "
+    "2,90851,90852 "
+    "1 2,90851,90852"
+  };
+  std::string err;
+  Command command = Command::Parse(input, 4, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::IMGDIFF, command.type());
+  ASSERT_EQ(4, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("51978f65035f584a8ef7afa941dacb6d5e862164", RangeSet({ { 90851, 90852 } })),
+            command.target());
+  ASSERT_EQ(SourceInfo("a6b1c49aed1b57a2aab1ec3e1505b945540cd8db", RangeSet({ { 90851, 90852 } }),
+                       RangeSet(), {}),
+            command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(29629269, 185), command.patch());
+}
+
+TEST(CommandsTest, Parse_MOVE) {
+  const std::string input{
+    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb "
+    "4,569884,569904,591946,592043 117 4,566779,566799,591946,592043"
+  };
+  std::string err;
+  Command command = Command::Parse(input, 5, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::MOVE, command.type());
+  ASSERT_EQ(5, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                       RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+            command.target());
+  ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                       RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), RangeSet(), {}),
+            command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_NEW) {
+  const std::string input{ "new 4,3,5,10,12" };
+  std::string err;
+  Command command = Command::Parse(input, 6, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::NEW, command.type());
+  ASSERT_EQ(6, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 3, 5 }, { 10, 12 } })), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_STASH) {
+  const std::string input{ "stash hash1 2,5,10" };
+  std::string err;
+  Command command = Command::Parse(input, 7, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::STASH, command.type());
+  ASSERT_EQ(7, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo(), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo("hash1", RangeSet({ { 5, 10 } })), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ZERO) {
+  const std::string input{ "zero 2,1,5" };
+  std::string err;
+  Command command = Command::Parse(input, 8, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::ZERO, command.type());
+  ASSERT_EQ(8, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 1, 5 } })), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_COMPUTE_HASH_TREE) {
+  const std::string input{ "compute_hash_tree 2,0,1 2,3,4 sha1 unknown-salt unknown-root-hash" };
+  std::string err;
+  Command command = Command::Parse(input, 9, &err);
+  ASSERT_TRUE(command);
+
+  ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, command.type());
+  ASSERT_EQ(9, command.index());
+  ASSERT_EQ(input, command.cmdline());
+
+  HashTreeInfo expected_info(RangeSet({ { 0, 1 } }), RangeSet({ { 3, 4 } }), "sha1", "unknown-salt",
+                             "unknown-root-hash");
+  ASSERT_EQ(expected_info, command.hash_tree_info());
+  ASSERT_EQ(TargetInfo(), command.target());
+  ASSERT_EQ(SourceInfo(), command.source());
+  ASSERT_EQ(StashInfo(), command.stash());
+  ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_InvalidNumberOfArgs) {
+  Command::abort_allowed_ = true;
+
+  // Note that the case of having excess args in BSDIFF, IMGDIFF and MOVE is covered by
+  // ParseTargetInfoAndSourceInfo_InvalidInput.
+  std::vector<std::string> inputs{
+    "abort foo",
+    "bsdiff",
+    "compute_hash_tree, 2,0,1 2,0,1 unknown-algorithm unknown-salt",
+    "erase",
+    "erase 4,3,5,10,12 hash1",
+    "free",
+    "free id1 id2",
+    "imgdiff",
+    "move",
+    "new",
+    "new 4,3,5,10,12 hash1",
+    "stash",
+    "stash id1",
+    "stash id1 4,3,5,10,12 id2",
+    "zero",
+    "zero 4,3,5,10,12 hash2",
+  };
+  for (const auto& input : inputs) {
+    std::string err;
+    ASSERT_FALSE(Command::Parse(input, 0, &err));
+  }
+}
+
+TEST(SourceInfoTest, Overlaps) {
+  ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                         RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                                       RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+  ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                         RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                                       RangeSet({ { 4, 7 }, { 16, 23 } }))));
+
+  ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                          RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+                   .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                                        RangeSet({ { 9, 16 } }))));
+}
+
+TEST(SourceInfoTest, Overlaps_EmptySourceOrTarget) {
+  ASSERT_FALSE(SourceInfo().Overlaps(TargetInfo()));
+
+  ASSERT_FALSE(SourceInfo().Overlaps(
+      TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+  ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                          RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+                   .Overlaps(TargetInfo()));
+}
+
+TEST(SourceInfoTest, Overlaps_WithStashes) {
+  ASSERT_FALSE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+                          RangeSet({ { 81, 175 }, { 265, 266 } }),  // source ranges
+                          RangeSet({ { 0, 94 }, { 95, 96 } }),      // source location
+                          { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+                                      RangeSet({ { 94, 95 } })) })
+                   .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                                        RangeSet({ { 175, 265 } }))));
+
+  ASSERT_TRUE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+                         RangeSet({ { 81, 175 }, { 265, 266 } }),  // source ranges
+                         RangeSet({ { 0, 94 }, { 95, 96 } }),      // source location
+                         { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+                                     RangeSet({ { 94, 95 } })) })
+                  .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+                                       RangeSet({ { 265, 266 } }))));
+}
+
+// The block size should be specified by the caller of ReadAll (i.e. from Command instance during
+// normal run).
+constexpr size_t kBlockSize = 4096;
+
+TEST(SourceInfoTest, ReadAll) {
+  // "2727756cfee3fbfe24bf5650123fd7743d7b3465" is the SHA-1 hex digest of 8192 * 'a'.
+  const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+                          {});
+  auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+    std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+    return 0;
+  };
+  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+  ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+  ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(buffer.data(), buffer.size(), digest);
+  ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_WithStashes) {
+  const SourceInfo source(
+      // SHA-1 hex digest of 8192 * 'a' + 4096 * 'b'.
+      "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+      { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+  auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+    std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+    return 0;
+  };
+  auto stash_reader = [](const std::string&, std::vector<uint8_t>* stash_buffer) -> int {
+    std::fill_n(stash_buffer->begin(), kBlockSize, 'b');
+    return 0;
+  };
+  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+  ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+  ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(buffer.data(), buffer.size(), digest);
+  ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_BufferTooSmall) {
+  const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+                          {});
+  auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+  std::vector<uint8_t> buffer(source.blocks() * kBlockSize - 1);
+  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+}
+
+TEST(SourceInfoTest, ReadAll_FailingReader) {
+  const SourceInfo source(
+      "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+      { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+  std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+  auto failing_block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return -1; };
+  auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader));
+
+  auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+  auto failing_stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return -1; };
+  ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader));
+}
+
+TEST(TransferListTest, Parse) {
+  std::vector<std::string> input_lines{
+    "4",  // version
+    "2",  // total blocks
+    "1",  // max stashed entries
+    "1",  // max stashed blocks
+    "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1 2,0,1",
+  };
+
+  std::string err;
+  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+  ASSERT_TRUE(static_cast<bool>(transfer_list));
+  ASSERT_EQ(4, transfer_list.version());
+  ASSERT_EQ(2, transfer_list.total_blocks());
+  ASSERT_EQ(1, transfer_list.stash_max_entries());
+  ASSERT_EQ(1, transfer_list.stash_max_blocks());
+  ASSERT_EQ(2U, transfer_list.commands().size());
+  ASSERT_EQ(Command::Type::STASH, transfer_list.commands()[0].type());
+  ASSERT_EQ(Command::Type::MOVE, transfer_list.commands()[1].type());
+}
+
+TEST(TransferListTest, Parse_InvalidCommand) {
+  std::vector<std::string> input_lines{
+    "4",  // version
+    "2",  // total blocks
+    "1",  // max stashed entries
+    "1",  // max stashed blocks
+    "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+    "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1",
+  };
+
+  std::string err;
+  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+  ASSERT_FALSE(static_cast<bool>(transfer_list));
+}
+
+TEST(TransferListTest, Parse_ZeroTotalBlocks) {
+  std::vector<std::string> input_lines{
+    "4",  // version
+    "0",  // total blocks
+    "0",  // max stashed entries
+    "0",  // max stashed blocks
+  };
+
+  std::string err;
+  TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+  ASSERT_TRUE(static_cast<bool>(transfer_list));
+  ASSERT_EQ(4, transfer_list.version());
+  ASSERT_EQ(0, transfer_list.total_blocks());
+  ASSERT_EQ(0, transfer_list.stash_max_entries());
+  ASSERT_EQ(0, transfer_list.stash_max_blocks());
+  ASSERT_TRUE(transfer_list.commands().empty());
+}
diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp
index 7f85d13..1ca786c 100644
--- a/tests/unit/dirutil_test.cpp
+++ b/tests/unit/dirutil_test.cpp
@@ -22,7 +22,8 @@
 
 #include <android-base/test_utils.h>
 #include <gtest/gtest.h>
-#include <otautil/DirUtil.h>
+
+#include "otautil/dirutil.h"
 
 TEST(DirUtilTest, create_invalid) {
   // Requesting to create an empty dir is invalid.
diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp
new file mode 100644
index 0000000..8061f3b
--- /dev/null
+++ b/tests/unit/parse_install_logs_test.cpp
@@ -0,0 +1,75 @@
+/*
+ * 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 <map>
+#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 "otautil/parse_install_logs.h"
+
+TEST(ParseInstallLogsTest, EmptyFile) {
+  TemporaryFile last_install;
+
+  auto metrics = ParseLastInstall(last_install.path);
+  ASSERT_TRUE(metrics.empty());
+}
+
+TEST(ParseInstallLogsTest, SideloadSmoke) {
+  TemporaryFile last_install;
+  ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path));
+  auto metrics = ParseLastInstall(last_install.path);
+  ASSERT_EQ(metrics.end(), metrics.find("ota_sideload"));
+
+  ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path));
+  metrics = ParseLastInstall(last_install.path);
+  ASSERT_NE(metrics.end(), metrics.find("ota_sideload"));
+}
+
+TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) {
+  std::vector<std::string> lines = {
+    "/sideload/package.zip",
+    "0",
+    "time_total: 300",
+    "uncrypt_time: 40",
+    "source_build: 4973410",
+    "bytes_written_system: " + std::to_string(1200 * 1024 * 1024),
+    "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024),
+    "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024),
+    "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024),
+    "temperature_start: 37000",
+    "temperature_end: 38000",
+    "temperature_max: 39000",
+    "error: 22",
+    "cause: 55",
+  };
+
+  auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+  std::map<std::string, int64_t> expected_result = {
+    { "ota_time_total", 300 },         { "ota_uncrypt_time", 40 },
+    { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 },
+    { "ota_stashed_in_MiBs", 350 },    { "ota_temperature_start", 37000 },
+    { "ota_temperature_end", 38000 },  { "ota_temperature_max", 39000 },
+    { "ota_non_ab_error_code", 22 },   { "ota_non_ab_cause_code", 55 },
+  };
+
+  ASSERT_EQ(expected_result, metrics);
+}
diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp
index 7ae193e..fc72f2f 100644
--- a/tests/unit/rangeset_test.cpp
+++ b/tests/unit/rangeset_test.cpp
@@ -209,6 +209,7 @@
   ASSERT_EQ(static_cast<size_t>(6), rs.GetBlockNumber(5));
   ASSERT_EQ(static_cast<size_t>(9), rs.GetBlockNumber(8));
 
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
   // Out of bound.
   ASSERT_EXIT(rs.GetBlockNumber(9), ::testing::KilledBySignal(SIGABRT), "");
 }
@@ -284,6 +285,8 @@
 
   ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106));
   ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10));
+
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
   // block#10 not in range.
   ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), "");
 }
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
new file mode 100644
index 0000000..7d97a00
--- /dev/null
+++ b/tests/unit/screen_ui_test.cpp
@@ -0,0 +1,494 @@
+/*
+ * 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 <stddef.h>
+#include <stdio.h>
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "common/test_constants.h"
+#include "device.h"
+#include "minui/minui.h"
+#include "otautil/paths.h"
+#include "private/resources.h"
+#include "screen_ui.h"
+
+static const std::vector<std::string> HEADERS{ "header" };
+static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" };
+
+TEST(ScreenUITest, StartPhoneMenuSmoke) {
+  Menu menu(false, 10, 20, HEADERS, ITEMS, 0);
+  ASSERT_FALSE(menu.scrollable());
+  ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
+  ASSERT_EQ(5u, menu.ItemsCount());
+
+  std::string message;
+  ASSERT_FALSE(menu.ItemsOverflow(&message));
+  for (size_t i = 0; i < menu.ItemsCount(); i++) {
+    ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+  }
+
+  ASSERT_EQ(0, menu.selection());
+}
+
+TEST(ScreenUITest, StartWearMenuSmoke) {
+  Menu menu(true, 10, 8, HEADERS, ITEMS, 1);
+  ASSERT_TRUE(menu.scrollable());
+  ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
+  ASSERT_EQ(5u, menu.ItemsCount());
+
+  std::string message;
+  ASSERT_FALSE(menu.ItemsOverflow(&message));
+  for (size_t i = 0; i < menu.ItemsCount() - 1; i++) {
+    ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+  }
+  // Test of the last item is truncated
+  ASSERT_EQ("12345678", menu.TextItem(4));
+  ASSERT_EQ(1, menu.selection());
+}
+
+TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
+  Menu menu(false, 1, 20, HEADERS, ITEMS, 0);
+  ASSERT_FALSE(menu.scrollable());
+  ASSERT_EQ(1u, menu.ItemsCount());
+
+  std::string message;
+  ASSERT_FALSE(menu.ItemsOverflow(&message));
+  for (size_t i = 0; i < menu.ItemsCount(); i++) {
+    ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+  }
+
+  ASSERT_EQ(0u, menu.MenuStart());
+  ASSERT_EQ(1u, menu.MenuEnd());
+}
+
+TEST(ScreenUITest, StartWearMenuItemsOverflow) {
+  Menu menu(true, 1, 20, HEADERS, ITEMS, 0);
+  ASSERT_TRUE(menu.scrollable());
+  ASSERT_EQ(5u, menu.ItemsCount());
+
+  std::string message;
+  ASSERT_TRUE(menu.ItemsOverflow(&message));
+  ASSERT_EQ("Current item: 1/5", message);
+
+  for (size_t i = 0; i < menu.ItemsCount(); i++) {
+    ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+  }
+
+  ASSERT_EQ(0u, menu.MenuStart());
+  ASSERT_EQ(1u, menu.MenuEnd());
+}
+
+TEST(ScreenUITest, PhoneMenuSelectSmoke) {
+  int sel = 0;
+  Menu menu(false, 10, 20, HEADERS, ITEMS, sel);
+  // Mimic down button 10 times (2 * items size)
+  for (int i = 0; i < 10; i++) {
+    sel = menu.Select(++sel);
+    ASSERT_EQ(sel, menu.selection());
+
+    // Wraps the selection for unscrollable menu when it reaches the boundary.
+    int expected = (i + 1) % 5;
+    ASSERT_EQ(expected, menu.selection());
+
+    ASSERT_EQ(0u, menu.MenuStart());
+    ASSERT_EQ(5u, menu.MenuEnd());
+  }
+
+  // Mimic up button 10 times
+  for (int i = 0; i < 10; i++) {
+    sel = menu.Select(--sel);
+    ASSERT_EQ(sel, menu.selection());
+
+    int expected = (9 - i) % 5;
+    ASSERT_EQ(expected, menu.selection());
+
+    ASSERT_EQ(0u, menu.MenuStart());
+    ASSERT_EQ(5u, menu.MenuEnd());
+  }
+}
+
+TEST(ScreenUITest, WearMenuSelectSmoke) {
+  int sel = 0;
+  Menu menu(true, 10, 20, HEADERS, ITEMS, sel);
+  // Mimic pressing down button 10 times (2 * items size)
+  for (int i = 0; i < 10; i++) {
+    sel = menu.Select(++sel);
+    ASSERT_EQ(sel, menu.selection());
+
+    // Stops the selection at the boundary if the menu is scrollable.
+    int expected = std::min(i + 1, 4);
+    ASSERT_EQ(expected, menu.selection());
+
+    ASSERT_EQ(0u, menu.MenuStart());
+    ASSERT_EQ(5u, menu.MenuEnd());
+  }
+
+  // Mimic pressing up button 10 times
+  for (int i = 0; i < 10; i++) {
+    sel = menu.Select(--sel);
+    ASSERT_EQ(sel, menu.selection());
+
+    int expected = std::max(3 - i, 0);
+    ASSERT_EQ(expected, menu.selection());
+
+    ASSERT_EQ(0u, menu.MenuStart());
+    ASSERT_EQ(5u, menu.MenuEnd());
+  }
+}
+
+TEST(ScreenUITest, WearMenuSelectItemsOverflow) {
+  int sel = 1;
+  Menu menu(true, 3, 20, HEADERS, ITEMS, sel);
+  ASSERT_EQ(5u, menu.ItemsCount());
+
+  // Scroll the menu to the end, and check the start & end of menu.
+  for (int i = 0; i < 3; i++) {
+    sel = menu.Select(++sel);
+    ASSERT_EQ(i + 2, sel);
+    ASSERT_EQ(static_cast<size_t>(i), menu.MenuStart());
+    ASSERT_EQ(static_cast<size_t>(i + 3), menu.MenuEnd());
+  }
+
+  // Press down button one more time won't change the MenuStart() and MenuEnd().
+  sel = menu.Select(++sel);
+  ASSERT_EQ(4, sel);
+  ASSERT_EQ(2u, menu.MenuStart());
+  ASSERT_EQ(5u, menu.MenuEnd());
+
+  // Scroll the menu to the top.
+  // The expected menu sel, start & ends are:
+  // sel 3, start 2, end 5
+  // sel 2, start 2, end 5
+  // sel 1, start 1, end 4
+  // sel 0, start 0, end 3
+  for (int i = 0; i < 4; i++) {
+    sel = menu.Select(--sel);
+    ASSERT_EQ(3 - i, sel);
+    ASSERT_EQ(static_cast<size_t>(std::min(3 - i, 2)), menu.MenuStart());
+    ASSERT_EQ(static_cast<size_t>(std::min(6 - i, 5)), menu.MenuEnd());
+  }
+
+  // Press up button one more time won't change the MenuStart() and MenuEnd().
+  sel = menu.Select(--sel);
+  ASSERT_EQ(0, sel);
+  ASSERT_EQ(0u, menu.MenuStart());
+  ASSERT_EQ(3u, menu.MenuEnd());
+}
+
+static constexpr int kMagicAction = 101;
+
+enum class KeyCode : int {
+  TIMEOUT = -1,
+  NO_OP = 0,
+  UP = 1,
+  DOWN = 2,
+  ENTER = 3,
+  MAGIC = 1001,
+  LAST,
+};
+
+static const std::map<KeyCode, int> kKeyMapping{
+  // clang-format off
+  { KeyCode::NO_OP, Device::kNoAction },
+  { KeyCode::UP, Device::kHighlightUp },
+  { KeyCode::DOWN, Device::kHighlightDown },
+  { KeyCode::ENTER, Device::kInvokeItem },
+  { KeyCode::MAGIC, kMagicAction },
+  // clang-format on
+};
+
+class TestableScreenRecoveryUI : public ScreenRecoveryUI {
+ public:
+  int WaitKey() override;
+
+  void SetKeyBuffer(const std::vector<KeyCode>& buffer);
+
+  int KeyHandler(int key, bool visible) const;
+
+  // The following functions expose the protected members for test purpose.
+  void RunLoadAnimation() {
+    LoadAnimation();
+  }
+
+  size_t GetLoopFrames() const {
+    return loop_frames;
+  }
+
+  size_t GetIntroFrames() const {
+    return intro_frames;
+  }
+
+  bool GetRtlLocale() const {
+    return rtl_locale_;
+  }
+
+ private:
+  std::vector<KeyCode> key_buffer_;
+  size_t key_buffer_index_;
+};
+
+void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) {
+  key_buffer_ = buffer;
+  key_buffer_index_ = 0;
+}
+
+int TestableScreenRecoveryUI::KeyHandler(int key, bool) const {
+  KeyCode key_code = static_cast<KeyCode>(key);
+  if (kKeyMapping.find(key_code) != kKeyMapping.end()) {
+    return kKeyMapping.at(key_code);
+  }
+  return Device::kNoAction;
+}
+
+int TestableScreenRecoveryUI::WaitKey() {
+  if (IsKeyInterrupted()) {
+    return static_cast<int>(RecoveryUI::KeyError::INTERRUPTED);
+  }
+
+  CHECK_LT(key_buffer_index_, key_buffer_.size());
+  return static_cast<int>(key_buffer_[key_buffer_index_++]);
+}
+
+class ScreenRecoveryUITest : public ::testing::Test {
+ protected:
+  const std::string kTestLocale = "en-US";
+  const std::string kTestRtlLocale = "ar";
+  const std::string kTestRtlLocaleWithSuffix = "ar-EG";
+
+  void SetUp() override {
+    has_graphics_ = gr_init() == 0;
+    gr_exit();
+
+    if (has_graphics_) {
+      ui_ = std::make_unique<TestableScreenRecoveryUI>();
+    }
+
+    testdata_dir_ = from_testdata_base("");
+    Paths::Get().set_resource_dir(testdata_dir_);
+    res_set_resource_dir(testdata_dir_);
+  }
+
+  bool has_graphics_;
+  std::unique_ptr<TestableScreenRecoveryUI> ui_;
+  std::string testdata_dir_;
+};
+
+#define RETURN_IF_NO_GRAPHICS                                                 \
+  do {                                                                        \
+    if (!has_graphics_) {                                                     \
+      GTEST_LOG_(INFO) << "Test skipped due to no available graphics device"; \
+      return;                                                                 \
+    }                                                                         \
+  } while (false)
+
+TEST_F(ScreenRecoveryUITest, Init) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ASSERT_EQ(kTestLocale, ui_->GetLocale());
+  ASSERT_FALSE(ui_->GetRtlLocale());
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ASSERT_FALSE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, dtor_NotCallingInit) {
+  ui_.reset();
+  ASSERT_FALSE(ui_);
+}
+
+TEST_F(ScreenRecoveryUITest, ShowText) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ui_->ShowText(true);
+  ASSERT_TRUE(ui_->IsTextVisible());
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+
+  ui_->ShowText(false);
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, RtlLocale) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestRtlLocale));
+  ASSERT_TRUE(ui_->GetRtlLocale());
+}
+
+TEST_F(ScreenRecoveryUITest, RtlLocaleWithSuffix) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix));
+  ASSERT_TRUE(ui_->GetRtlLocale());
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::UP,
+      KeyCode::NO_OP,
+      KeyCode::NO_OP,
+      KeyCode::UP,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ui_->SetKeyBuffer({
+      KeyCode::MAGIC,
+  });
+  ASSERT_EQ(static_cast<size_t>(kMagicAction),
+            ui_->ShowMenu(HEADERS, ITEMS, 3, false,
+                          std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                    std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ui_->SetKeyBuffer({
+      KeyCode::TIMEOUT,
+  });
+  ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT),
+            ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ui_->ShowText(true);
+  ui_->ShowText(false);
+  ASSERT_TRUE(ui_->WasTextEverVisible());
+
+  ui_->SetKeyBuffer({
+      KeyCode::TIMEOUT,
+      KeyCode::DOWN,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+                              std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                        std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenuWithInterrupt) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::UP,
+      KeyCode::DOWN,
+      KeyCode::ENTER,
+  });
+
+  ui_->InterruptKey();
+  ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED),
+            ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+                          std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                    std::placeholders::_1, std::placeholders::_2)));
+
+  ui_->SetKeyBuffer({
+      KeyCode::UP,
+      KeyCode::UP,
+      KeyCode::NO_OP,
+      KeyCode::NO_OP,
+      KeyCode::UP,
+      KeyCode::ENTER,
+  });
+  ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED),
+            ui_->ShowMenu(HEADERS, ITEMS, 0, true,
+                          std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+                                    std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, LoadAnimation) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  // Make a few copies of loop00000.png from testdata.
+  std::string image_data;
+  ASSERT_TRUE(android::base::ReadFileToString(testdata_dir_ + "/loop00000.png", &image_data));
+
+  std::vector<std::string> tempfiles;
+  TemporaryDir resource_dir;
+  for (const auto& name : { "00002", "00100", "00050" }) {
+    tempfiles.push_back(android::base::StringPrintf("%s/loop%s.png", resource_dir.path, name));
+    ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back()));
+  }
+  for (const auto& name : { "00", "01" }) {
+    tempfiles.push_back(android::base::StringPrintf("%s/intro%s.png", resource_dir.path, name));
+    ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back()));
+  }
+  Paths::Get().set_resource_dir(resource_dir.path);
+
+  ui_->RunLoadAnimation();
+
+  ASSERT_EQ(2u, ui_->GetIntroFrames());
+  ASSERT_EQ(3u, ui_->GetLoopFrames());
+
+  for (const auto& name : tempfiles) {
+    ASSERT_EQ(0, unlink(name.c_str()));
+  }
+}
+
+TEST_F(ScreenRecoveryUITest, LoadAnimation_MissingAnimation) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  // We need a dir that doesn't contain any animation. However, using TemporaryDir will give
+  // leftovers since this is a death test where TemporaryDir::~TemporaryDir() won't be called.
+  Paths::Get().set_resource_dir("/proc/self");
+
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
+  ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), "");
+}
+
+#undef RETURN_IF_NO_GRAPHICS
diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp
index 434ee25..de8ff70 100644
--- a/tests/unit/sysutil_test.cpp
+++ b/tests/unit/sysutil_test.cpp
@@ -14,14 +14,13 @@
  * limitations under the License.
  */
 
-#include <gtest/gtest.h>
-
 #include <string>
 
 #include <android-base/file.h>
 #include <android-base/test_utils.h>
+#include <gtest/gtest.h>
 
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
 
 TEST(SysUtilTest, InvalidArgs) {
   MemMapping mapping;
@@ -128,3 +127,13 @@
   ASSERT_TRUE(android::base::WriteStringToFile("/doesntexist\n4096 4096\n1\n0 1\n", temp_file.path));
   ASSERT_FALSE(mapping.MapFile(filename));
 }
+
+TEST(SysUtilTest, StringVectorToNullTerminatedArray) {
+  std::vector<std::string> args{ "foo", "bar", "baz" };
+  auto args_with_nullptr = StringVectorToNullTerminatedArray(args);
+  ASSERT_EQ(4, args_with_nullptr.size());
+  ASSERT_STREQ("foo", args_with_nullptr[0]);
+  ASSERT_STREQ("bar", args_with_nullptr[1]);
+  ASSERT_STREQ("baz", args_with_nullptr[2]);
+  ASSERT_EQ(nullptr, args_with_nullptr[3]);
+}
diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp
index 8276685..47f33d9 100644
--- a/tests/unit/zip_test.cpp
+++ b/tests/unit/zip_test.cpp
@@ -23,10 +23,10 @@
 #include <android-base/file.h>
 #include <android-base/test_utils.h>
 #include <gtest/gtest.h>
-#include <otautil/SysUtil.h>
 #include <ziparchive/zip_archive.h>
 
 #include "common/test_constants.h"
+#include "otautil/sysutil.h"
 
 TEST(ZipTest, OpenFromMemory) {
   std::string zip_path = from_testdata_base("ziptest_dummy-update.zip");
diff --git a/tools/Android.mk b/tools/Android.mk
deleted file mode 100644
index 6571161..0000000
--- a/tools/Android.mk
+++ /dev/null
@@ -1 +0,0 @@
-include $(all-subdir-makefiles)
diff --git a/tools/dumpkey/Android.bp b/tools/dumpkey/Android.bp
new file mode 100644
index 0000000..eb45e31
--- /dev/null
+++ b/tools/dumpkey/Android.bp
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "dumpkey",
+
+    manifest: "DumpPublicKey.mf",
+
+    srcs: [
+        "DumpPublicKey.java",
+    ],
+
+    static_libs: [
+        "bouncycastle-host",
+    ],
+}
diff --git a/tools/recovery_l10n/Android.bp b/tools/recovery_l10n/Android.bp
new file mode 100644
index 0000000..d0a6d4b
--- /dev/null
+++ b/tools/recovery_l10n/Android.bp
@@ -0,0 +1,23 @@
+// 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.
+
+android_app {
+    name: "RecoveryLocalizer",
+
+    sdk_version: "current",
+
+    srcs: [
+        "src/**/*.java",
+    ],
+}
diff --git a/tools/recovery_l10n/Android.mk b/tools/recovery_l10n/Android.mk
deleted file mode 100644
index 7197c5c..0000000
--- a/tools/recovery_l10n/Android.mk
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2012 Google Inc. All Rights Reserved.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := RecoveryLocalizer
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-include $(BUILD_PACKAGE)
diff --git a/ui.cpp b/ui.cpp
index 3c9ded7..14b5b09 100644
--- a/ui.cpp
+++ b/ui.cpp
@@ -18,49 +18,52 @@
 
 #include <errno.h>
 #include <fcntl.h>
-#include <linux/input.h>
-#include <pthread.h>
-#include <stdarg.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
-#include <sys/stat.h>
 #include <sys/time.h>
 #include <sys/types.h>
 #include <time.h>
 #include <unistd.h>
 
+#include <chrono>
 #include <functional>
 #include <string>
+#include <thread>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/parseint.h>
 #include <android-base/properties.h>
 #include <android-base/strings.h>
-#include <cutils/android_reboot.h>
-#include <minui/minui.h>
 
-#include "common.h"
+#include "minui/minui.h"
+#include "otautil/sysutil.h"
 #include "roots.h"
-#include "device.h"
 
-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 =
+using namespace std::chrono_literals;
+
+constexpr int UI_WAIT_KEY_TIMEOUT_SEC = 120;
+constexpr const char* BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/brightness";
+constexpr const char* MAX_BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/max_brightness";
+constexpr const char* BRIGHTNESS_FILE_SDM = "/sys/class/backlight/panel0-backlight/brightness";
+constexpr const char* MAX_BRIGHTNESS_FILE_SDM =
     "/sys/class/backlight/panel0-backlight/max_brightness";
 
+constexpr int kDefaultTouchLowThreshold = 50;
+constexpr int kDefaultTouchHighThreshold = 90;
+
 RecoveryUI::RecoveryUI()
     : 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),
+      touch_low_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_low_threshold",
+                                                         kDefaultTouchLowThreshold)),
+      touch_high_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_high_threshold",
+                                                          kDefaultTouchHighThreshold)),
+      key_interrupted_(false),
       key_queue_len(0),
       key_last_down(-1),
       key_long_press(false),
@@ -75,11 +78,17 @@
       touch_slot_(0),
       is_bootreason_recovery_ui_(false),
       screensaver_state_(ScreensaverState::DISABLED) {
-  pthread_mutex_init(&key_queue_mutex, nullptr);
-  pthread_cond_init(&key_queue_cond, nullptr);
   memset(key_pressed, 0, sizeof(key_pressed));
 }
 
+RecoveryUI::~RecoveryUI() {
+  ev_exit();
+  input_thread_stopped_ = true;
+  if (input_thread_.joinable()) {
+    input_thread_.join();
+  }
+}
+
 void RecoveryUI::OnKeyDetected(int key_code) {
   if (key_code == KEY_POWER) {
     has_power_key = true;
@@ -92,16 +101,6 @@
   }
 }
 
-// Reads input events, handles special hot keys, and adds to the key queue.
-static void* InputThreadLoop(void*) {
-  while (true) {
-    if (!ev_wait(-1)) {
-      ev_dispatch();
-    }
-  }
-  return nullptr;
-}
-
 bool RecoveryUI::InitScreensaver() {
   // Disabled.
   if (brightness_normal_ == 0 || brightness_dimmed_ > brightness_normal_) {
@@ -168,7 +167,15 @@
     LOG(INFO) << "Screensaver disabled";
   }
 
-  pthread_create(&input_thread_, nullptr, InputThreadLoop, nullptr);
+  // Create a separate thread that handles input events.
+  input_thread_ = std::thread([this]() {
+    while (!this->input_thread_stopped_) {
+      if (!ev_wait(500)) {
+        ev_dispatch();
+      }
+    }
+  });
+
   return true;
 }
 
@@ -176,15 +183,15 @@
   enum SwipeDirection { UP, DOWN, RIGHT, LEFT } direction;
 
   // We only consider a valid swipe if:
-  // - the delta along one axis is below kTouchLowThreshold;
-  // - and the delta along the other axis is beyond kTouchHighThreshold.
-  if (abs(dy) < kTouchLowThreshold && abs(dx) > kTouchHighThreshold) {
+  // - the delta along one axis is below touch_low_threshold_;
+  // - and the delta along the other axis is beyond touch_high_threshold_.
+  if (abs(dy) < touch_low_threshold_ && abs(dx) > touch_high_threshold_) {
     direction = dx < 0 ? SwipeDirection::LEFT : SwipeDirection::RIGHT;
-  } else if (abs(dx) < kTouchLowThreshold && abs(dy) > kTouchHighThreshold) {
+  } else if (abs(dx) < touch_low_threshold_ && abs(dy) > touch_high_threshold_) {
     direction = dy < 0 ? SwipeDirection::UP : SwipeDirection::DOWN;
   } else {
-    LOG(DEBUG) << "Ignored " << dx << " " << dy << " (low: " << kTouchLowThreshold
-               << ", high: " << kTouchHighThreshold << ")";
+    LOG(DEBUG) << "Ignored " << dx << " " << dy << " (low: " << touch_low_threshold_
+               << ", high: " << touch_high_threshold_ << ")";
     return;
   }
 
@@ -325,46 +332,38 @@
   return 0;
 }
 
-// Process a key-up or -down event.  A key is "registered" when it is
-// pressed and then released, with no other keypresses or releases in
-// between.  Registered keys are passed to CheckKey() to see if it
-// should trigger a visibility toggle, an immediate reboot, or be
-// queued to be processed next time the foreground thread wants a key
-// (eg, for the menu).
+// Processes a key-up or -down event. A key is "registered" when it is pressed and then released,
+// with no other keypresses or releases in between. Registered keys are passed to CheckKey() to
+// see if it should trigger a visibility toggle, an immediate reboot, or be queued to be processed
+// next time the foreground thread wants a key (eg, for the menu).
 //
-// We also keep track of which keys are currently down so that
-// CheckKey can call IsKeyPressed to see what other keys are held when
-// a key is registered.
+// We also keep track of which keys are currently down so that CheckKey() can call IsKeyPressed()
+// to see what other keys are held when a key is registered.
 //
 // updown == 1 for key down events; 0 for key up events
 void RecoveryUI::ProcessKey(int key_code, int updown) {
   bool register_key = false;
   bool long_press = false;
-  bool reboot_enabled;
 
-  pthread_mutex_lock(&key_queue_mutex);
-  key_pressed[key_code] = updown;
-  if (updown) {
-    ++key_down_count;
-    key_last_down = key_code;
-    key_long_press = false;
-    key_timer_t* info = new key_timer_t;
-    info->ui = this;
-    info->key_code = key_code;
-    info->count = key_down_count;
-    pthread_t thread;
-    pthread_create(&thread, nullptr, &RecoveryUI::time_key_helper, info);
-    pthread_detach(thread);
-  } else {
-    if (key_last_down == key_code) {
-      long_press = key_long_press;
-      register_key = true;
+  {
+    std::lock_guard<std::mutex> lg(key_queue_mutex);
+    key_pressed[key_code] = updown;
+    if (updown) {
+      ++key_down_count;
+      key_last_down = key_code;
+      key_long_press = false;
+      std::thread time_key_thread(&RecoveryUI::TimeKey, this, key_code, key_down_count);
+      time_key_thread.detach();
+    } else {
+      if (key_last_down == key_code) {
+        long_press = key_long_press;
+        register_key = true;
+      }
+      key_last_down = -1;
     }
-    key_last_down = -1;
   }
-  reboot_enabled = enable_reboot;
-  pthread_mutex_unlock(&key_queue_mutex);
 
+  bool reboot_enabled = enable_reboot;
   if (register_key) {
     switch (CheckKey(key_code, long_press)) {
       case RecoveryUI::IGNORE:
@@ -390,67 +389,87 @@
   }
 }
 
-void* RecoveryUI::time_key_helper(void* cookie) {
-  key_timer_t* info = static_cast<key_timer_t*>(cookie);
-  info->ui->time_key(info->key_code, info->count);
-  delete info;
-  return nullptr;
-}
-
-void RecoveryUI::time_key(int key_code, int count) {
-  usleep(750000);  // 750 ms == "long"
+void RecoveryUI::TimeKey(int key_code, int count) {
+  std::this_thread::sleep_for(750ms);  // 750 ms == "long"
   bool long_press = false;
-  pthread_mutex_lock(&key_queue_mutex);
-  if (key_last_down == key_code && key_down_count == count) {
-    long_press = key_long_press = true;
+  {
+    std::lock_guard<std::mutex> lg(key_queue_mutex);
+    if (key_last_down == key_code && key_down_count == count) {
+      long_press = key_long_press = true;
+    }
   }
-  pthread_mutex_unlock(&key_queue_mutex);
   if (long_press) KeyLongPress(key_code);
 }
 
 void RecoveryUI::EnqueueKey(int key_code) {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::lock_guard<std::mutex> lg(key_queue_mutex);
   const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
   if (key_queue_len < queue_max) {
     key_queue[key_queue_len++] = key_code;
-    pthread_cond_signal(&key_queue_cond);
+    key_queue_cond.notify_one();
   }
-  pthread_mutex_unlock(&key_queue_mutex);
+}
+
+void RecoveryUI::SetScreensaverState(ScreensaverState state) {
+  switch (state) {
+    case ScreensaverState::NORMAL:
+      if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
+                                           brightness_file_)) {
+        screensaver_state_ = ScreensaverState::NORMAL;
+        LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
+                  << "%)";
+      } else {
+        LOG(ERROR) << "Unable to set brightness to normal";
+      }
+      break;
+    case ScreensaverState::DIMMED:
+      if (android::base::WriteStringToFile(std::to_string(brightness_dimmed_value_),
+                                           brightness_file_)) {
+        LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
+                  << "%)";
+        screensaver_state_ = ScreensaverState::DIMMED;
+      } else {
+        LOG(ERROR) << "Unable to set brightness to dim";
+      }
+      break;
+    case ScreensaverState::OFF:
+      if (android::base::WriteStringToFile("0", brightness_file_)) {
+        LOG(INFO) << "Brightness: 0 (off)";
+        screensaver_state_ = ScreensaverState::OFF;
+      } else {
+        LOG(ERROR) << "Unable to set brightness to off";
+      }
+      break;
+    default:
+      LOG(ERROR) << "Invalid screensaver state";
+  }
 }
 
 int RecoveryUI::WaitKey() {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::unique_lock<std::mutex> lk(key_queue_mutex);
 
-  // Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is
-  // plugged in.
+  // Check for a saved key queue interruption.
+  if (key_interrupted_) {
+    SetScreensaverState(ScreensaverState::NORMAL);
+    return static_cast<int>(KeyError::INTERRUPTED);
+  }
+
+  // Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is plugged in.
   do {
-    struct timeval now;
-    struct timespec timeout;
-    gettimeofday(&now, nullptr);
-    timeout.tv_sec = now.tv_sec;
-    timeout.tv_nsec = now.tv_usec * 1000;
-    timeout.tv_sec += UI_WAIT_KEY_TIMEOUT_SEC;
-
-    int rc = 0;
-    while (key_queue_len == 0 && rc != ETIMEDOUT) {
-      rc = pthread_cond_timedwait(&key_queue_cond, &key_queue_mutex, &timeout);
+    bool rc = key_queue_cond.wait_for(lk, std::chrono::seconds(UI_WAIT_KEY_TIMEOUT_SEC), [this] {
+      return this->key_queue_len != 0 || key_interrupted_;
+    });
+    if (key_interrupted_) {
+      SetScreensaverState(ScreensaverState::NORMAL);
+      return static_cast<int>(KeyError::INTERRUPTED);
     }
-
     if (screensaver_state_ != ScreensaverState::DISABLED) {
-      if (rc == ETIMEDOUT) {
-        // Lower the brightness level: NORMAL -> DIMMED; DIMMED -> OFF.
+      if (!rc) {
+        // Must be after a timeout. 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_)) {
-            LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
-                      << "%)";
-            screensaver_state_ = ScreensaverState::DIMMED;
-          }
+          SetScreensaverState(ScreensaverState::DIMMED);
         } else if (screensaver_state_ == ScreensaverState::DIMMED) {
-          if (android::base::WriteStringToFile("0", brightness_file_)) {
-            LOG(INFO) << "Brightness: 0 (off)";
-            screensaver_state_ = ScreensaverState::OFF;
-          }
+          SetScreensaverState(ScreensaverState::OFF);
         }
       } else if (screensaver_state_ != ScreensaverState::NORMAL) {
         // Drop the first key if it's changing from OFF to NORMAL.
@@ -461,25 +480,27 @@
         }
 
         // Reset the brightness to normal.
-        if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
-                                             brightness_file_)) {
-          screensaver_state_ = ScreensaverState::NORMAL;
-          LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
-                    << "%)";
-        }
+        SetScreensaverState(ScreensaverState::NORMAL);
       }
     }
   } while (IsUsbConnected() && key_queue_len == 0);
 
-  int key = -1;
+  int key = static_cast<int>(KeyError::TIMED_OUT);
   if (key_queue_len > 0) {
     key = key_queue[0];
     memcpy(&key_queue[0], &key_queue[1], sizeof(int) * --key_queue_len);
   }
-  pthread_mutex_unlock(&key_queue_mutex);
   return key;
 }
 
+void RecoveryUI::InterruptKey() {
+  {
+    std::lock_guard<std::mutex> lg(key_queue_mutex);
+    key_interrupted_ = true;
+  }
+  key_queue_cond.notify_one();
+}
+
 bool RecoveryUI::IsUsbConnected() {
   int fd = open("/sys/class/android_usb/android0/state", O_RDONLY);
   if (fd < 0) {
@@ -497,16 +518,14 @@
 }
 
 bool RecoveryUI::IsKeyPressed(int key) {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::lock_guard<std::mutex> lg(key_queue_mutex);
   int pressed = key_pressed[key];
-  pthread_mutex_unlock(&key_queue_mutex);
   return pressed;
 }
 
 bool RecoveryUI::IsLongPress() {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::lock_guard<std::mutex> lg(key_queue_mutex);
   bool result = key_long_press;
-  pthread_mutex_unlock(&key_queue_mutex);
   return result;
 }
 
@@ -523,15 +542,15 @@
 }
 
 void RecoveryUI::FlushKeys() {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::lock_guard<std::mutex> lg(key_queue_mutex);
   key_queue_len = 0;
-  pthread_mutex_unlock(&key_queue_mutex);
 }
 
 RecoveryUI::KeyAction RecoveryUI::CheckKey(int key, bool is_long_press) {
-  pthread_mutex_lock(&key_queue_mutex);
-  key_long_press = false;
-  pthread_mutex_unlock(&key_queue_mutex);
+  {
+    std::lock_guard<std::mutex> lg(key_queue_mutex);
+    key_long_press = false;
+  }
 
   // If we have power and volume up keys, that chord is the signal to toggle the text display.
   if (HasThreeButtons() || (HasPowerKey() && HasTouchScreen() && touch_screen_allowed_)) {
@@ -554,9 +573,7 @@
 
   // Press power seven times in a row to reboot.
   if (key == KEY_POWER) {
-    pthread_mutex_lock(&key_queue_mutex);
     bool reboot_enabled = enable_reboot;
-    pthread_mutex_unlock(&key_queue_mutex);
 
     if (reboot_enabled) {
       ++consecutive_power_keys;
@@ -576,7 +593,6 @@
 }
 
 void RecoveryUI::SetEnableReboot(bool enabled) {
-  pthread_mutex_lock(&key_queue_mutex);
+  std::lock_guard<std::mutex> lg(key_queue_mutex);
   enable_reboot = enabled;
-  pthread_mutex_unlock(&key_queue_mutex);
 }
diff --git a/ui.h b/ui.h
index 4c54d69..e0fb13e 100644
--- a/ui.h
+++ b/ui.h
@@ -17,11 +17,15 @@
 #ifndef RECOVERY_UI_H
 #define RECOVERY_UI_H
 
-#include <linux/input.h>
-#include <pthread.h>
-#include <time.h>
+#include <linux/input.h>  // KEY_MAX
 
+#include <atomic>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
 #include <string>
+#include <thread>
+#include <vector>
 
 // Abstract class for controlling the user interface during recovery.
 class RecoveryUI {
@@ -47,14 +51,21 @@
     IGNORE
   };
 
+  enum class KeyError : int {
+    TIMED_OUT = -1,
+    INTERRUPTED = -2,
+  };
+
   RecoveryUI();
 
-  virtual ~RecoveryUI() {}
+  virtual ~RecoveryUI();
 
   // Initializes the object; called before anything else. UI texts will be initialized according to
   // the given locale. Returns true on success.
   virtual bool Init(const std::string& locale);
 
+  virtual std::string GetLocale() const = 0;
+
   // Shows a stage indicator. Called immediately after Init().
   virtual void SetStage(int current, int max) = 0;
 
@@ -87,13 +98,19 @@
   virtual void Print(const char* fmt, ...) __printflike(2, 3) = 0;
   virtual void PrintOnScreenOnly(const char* fmt, ...) __printflike(2, 3) = 0;
 
-  virtual void ShowFile(const char* filename) = 0;
+  // Shows the contents of the given file. Caller ensures the patition that contains the file has
+  // been mounted.
+  virtual void ShowFile(const std::string& filename) = 0;
 
   // --- key handling ---
 
-  // Waits for a key and return it. May return -1 after timeout.
+  // Waits for a key and return it. May return TIMED_OUT after timeout and
+  // KeyError::INTERRUPTED on a key interrupt.
   virtual int WaitKey();
 
+  // Wakes up the UI if it is waiting on key input, causing WaitKey to return KeyError::INTERRUPTED.
+  virtual void InterruptKey();
+
   virtual bool IsKeyPressed(int key);
   virtual bool IsLongPress();
 
@@ -128,17 +145,32 @@
 
   // --- menu display ---
 
-  // Display some header text followed by a menu of items, which appears at the top of the screen
-  // (in place of any scrolling ui_print() output, if necessary).
-  virtual void StartMenu(const char* const* headers, const char* const* items,
-                         int initial_selection) = 0;
+  virtual void SetTitle(const std::vector<std::string>& lines) = 0;
 
-  // Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
-  // selected.
-  virtual int SelectMenu(int sel) = 0;
+  // Displays a menu with the given 'headers' and 'items'. The supplied 'key_handler' callback,
+  // which is typically bound to Device::HandleMenuKey(), should return the expected action for the
+  // given key code and menu visibility (e.g. to move the cursor or to select an item). Caller sets
+  // 'menu_only' to true to ensure only a menu item gets selected and returned. Otherwise if
+  // 'menu_only' is false, ShowMenu() will forward any non-negative value returned from the
+  // key_handler, which may be beyond the range of menu items. This could be used to trigger a
+  // device-specific action, even without that being listed in the menu. Caller needs to handle
+  // such a case accordingly (e.g. by calling Device::InvokeMenuItem() to process the action).
+  // Returns a non-negative value (the chosen item number or device-specific action code), or
+  // static_cast<size_t>(TIMED_OUT) if timed out waiting for input or
+  // static_cast<size_t>(ERR_KEY_INTERTUPT) if interrupted, such as by InterruptKey().
+  virtual size_t ShowMenu(const std::vector<std::string>& headers,
+                          const std::vector<std::string>& items, size_t initial_selection,
+                          bool menu_only, const std::function<int(int, bool)>& key_handler) = 0;
 
-  // Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed.
-  virtual void EndMenu() = 0;
+  // Resets the key interrupt status.
+  void ResetKeyInterruptStatus() {
+    key_interrupted_ = false;
+  }
+
+  // Returns the key interrupt status.
+  bool IsKeyInterrupted() const {
+    return key_interrupted_;
+  }
 
  protected:
   void EnqueueKey(int key_code);
@@ -162,31 +194,24 @@
     OFF
   };
 
-  struct key_timer_t {
-    RecoveryUI* ui;
-    int key_code;
-    int count;
-  };
-
   // The sensitivity when detecting a swipe.
-  const int kTouchLowThreshold;
-  const int kTouchHighThreshold;
+  const int touch_low_threshold_;
+  const int touch_high_threshold_;
 
   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);
+  void TimeKey(int key_code, int count);
 
   bool IsUsbConnected();
 
-  static void* time_key_helper(void* cookie);
-  void time_key(int key_code, int count);
-
   bool InitScreensaver();
-
+  void SetScreensaverState(ScreensaverState state);
   // Key event input queue
-  pthread_mutex_t key_queue_mutex;
-  pthread_cond_t key_queue_cond;
+  std::mutex key_queue_mutex;
+  std::condition_variable key_queue_cond;
+  bool key_interrupted_;
   int key_queue[256], key_queue_len;
   char key_pressed[KEY_MAX + 1];  // under key_queue_mutex
   int key_last_down;              // under key_queue_mutex
@@ -213,7 +238,8 @@
   bool touch_swiping_;
   bool is_bootreason_recovery_ui_;
 
-  pthread_t input_thread_;
+  std::thread input_thread_;
+  std::atomic<bool> input_thread_stopped_{ false };
 
   ScreensaverState screensaver_state_;
 
diff --git a/uncrypt/Android.bp b/uncrypt/Android.bp
index aa56d2f..107a7f0 100644
--- a/uncrypt/Android.bp
+++ b/uncrypt/Android.bp
@@ -24,13 +24,15 @@
         "-Werror",
     ],
 
-    static_libs: [
-        "libbootloader_message",
-        "libotautil",
-        "libfs_mgr",
+    shared_libs: [
         "libbase",
+        "libbootloader_message",
         "libcutils",
-        "liblog",
+        "libfs_mgr",
+    ],
+
+    static_libs: [
+        "libotautil",
     ],
 
     init_rc: [
diff --git a/uncrypt/uncrypt.cpp b/uncrypt/uncrypt.cpp
index bb43c2c..95f40c7 100644
--- a/uncrypt/uncrypt.cpp
+++ b/uncrypt/uncrypt.cpp
@@ -172,14 +172,15 @@
     return fstab;
 }
 
-static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted, bool *f2fs_fs) {
+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;
+    // ensure f2fs_fs is set to false first.
+    *f2fs_fs = false;
+
     for (int i = 0; i < fstab->num_entries; ++i) {
         struct fstab_rec* v = &fstab->recs[i];
         if (!v->mount_point) {
@@ -196,8 +197,9 @@
                     *encrypted = true;
                 }
             }
-            if (f2fs_fs && strcmp(v->fs_type, "f2fs") == 0)
+            if (strcmp(v->fs_type, "f2fs") == 0) {
                 *f2fs_fs = true;
+            }
             return v->blk_device;
         }
     }
@@ -313,15 +315,30 @@
         }
     }
 
-#ifndef F2FS_IOC_SET_DONTMOVE
+// F2FS-specific ioctl
+// It requires the below kernel commit merged in v4.16-rc1.
+//   1ad71a27124c ("f2fs: add an ioctl to disable GC for specific file")
+// In android-4.4,
+//   56ee1e817908 ("f2fs: updates on v4.16-rc1")
+// In android-4.9,
+//   2f17e34672a8 ("f2fs: updates on v4.16-rc1")
+// In android-4.14,
+//   ce767d9a55bc ("f2fs: updates on v4.16-rc1")
+#ifndef F2FS_IOC_SET_PIN_FILE
 #ifndef F2FS_IOCTL_MAGIC
 #define F2FS_IOCTL_MAGIC		0xf5
 #endif
-#define F2FS_IOC_SET_DONTMOVE		_IO(F2FS_IOCTL_MAGIC, 13)
+#define F2FS_IOC_SET_PIN_FILE	_IOW(F2FS_IOCTL_MAGIC, 13, __u32)
+#define F2FS_IOC_GET_PIN_FILE	_IOW(F2FS_IOCTL_MAGIC, 14, __u32)
 #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;
+    if (f2fs_fs) {
+        __u32 set = 1;
+        int error = ioctl(fd, F2FS_IOC_SET_PIN_FILE, &set);
+        // Don't break the old kernels which don't support it.
+        if (error && errno != ENOTTY && errno != ENOTSUP) {
+            PLOG(ERROR) << "Failed to set pin_file for f2fs: " << path << " on " << blk_dev;
+            return kUncryptIoctlError;
+        }
     }
 
     off64_t pos = 0;
diff --git a/update_verifier/Android.bp b/update_verifier/Android.bp
new file mode 100644
index 0000000..1b84619
--- /dev/null
+++ b/update_verifier/Android.bp
@@ -0,0 +1,118 @@
+// 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.
+
+cc_defaults {
+    name: "update_verifier_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    local_include_dirs: [
+        "include",
+    ],
+}
+
+cc_library_static {
+    name: "libupdate_verifier",
+
+    defaults: [
+        "update_verifier_defaults",
+    ],
+
+    srcs: [
+        "care_map.proto",
+        "update_verifier.cpp",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+
+    static_libs: [
+        "libotautil",
+    ],
+
+    shared_libs: [
+        "android.hardware.boot@1.0",
+        "libbase",
+        "libcutils",
+    ],
+
+    proto: {
+        type: "lite",
+        export_proto_headers: true,
+    },
+}
+
+cc_binary {
+    name: "update_verifier",
+
+    defaults: [
+        "update_verifier_defaults",
+    ],
+
+    srcs: [
+        "update_verifier_main.cpp",
+    ],
+
+    static_libs: [
+        "libupdate_verifier",
+        "libotautil",
+    ],
+
+    shared_libs: [
+        "android.hardware.boot@1.0",
+        "libbase",
+        "libcutils",
+        "libhardware",
+        "libhidlbase",
+        "liblog",
+        "libprotobuf-cpp-lite",
+        "libutils",
+    ],
+
+    init_rc: [
+        "update_verifier.rc",
+    ],
+}
+
+python_binary_host {
+    name: "care_map_generator",
+
+    srcs: [
+        "care_map_generator.py",
+        "care_map.proto",
+    ],
+    libs: [
+        "python-symbol",
+        // Soong won't add "libprotobuf-python" to the dependencies if
+        // filegroup contains .proto files. So add it here explicitly.
+        "libprotobuf-python",
+    ],
+    proto: {
+        canonical_path_from_root: false,
+    },
+
+    version: {
+        py2: {
+            enabled: true,
+            embedded_launcher: true,
+        },
+        py3: {
+            enabled: false,
+            embedded_launcher: false,
+        },
+    },
+}
diff --git a/update_verifier/Android.mk b/update_verifier/Android.mk
deleted file mode 100644
index 0ff8854..0000000
--- a/update_verifier/Android.mk
+++ /dev/null
@@ -1,77 +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.
-
-LOCAL_PATH := $(call my-dir)
-
-# libupdate_verifier (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    update_verifier.cpp
-
-LOCAL_MODULE := libupdate_verifier
-
-LOCAL_STATIC_LIBRARIES := \
-    libotautil
-
-LOCAL_SHARED_LIBRARIES := \
-    libbase \
-    libcutils \
-    android.hardware.boot@1.0
-
-LOCAL_CFLAGS := -Wall -Werror
-
-LOCAL_EXPORT_C_INCLUDE_DIRS := \
-    $(LOCAL_PATH)/include
-
-LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/include
-
-ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true)
-LOCAL_CFLAGS += -DPRODUCT_SUPPORTS_VERITY=1
-endif
-
-ifeq ($(BOARD_AVB_ENABLE),true)
-LOCAL_CFLAGS += -DBOARD_AVB_ENABLE=1
-endif
-
-include $(BUILD_STATIC_LIBRARY)
-
-# update_verifier (executable)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
-    update_verifier_main.cpp
-
-LOCAL_MODULE := update_verifier
-LOCAL_STATIC_LIBRARIES := \
-    libupdate_verifier \
-    libotautil
-
-LOCAL_SHARED_LIBRARIES := \
-    libbase \
-    libcutils \
-    libhardware \
-    liblog \
-    libutils \
-    libhidlbase \
-    android.hardware.boot@1.0
-
-LOCAL_CFLAGS := -Wall -Werror
-
-LOCAL_INIT_RC := update_verifier.rc
-
-include $(BUILD_EXECUTABLE)
diff --git a/mounts.h b/update_verifier/care_map.proto
similarity index 63%
copy from mounts.h
copy to update_verifier/care_map.proto
index 0de1ebd..15d3afa 100644
--- a/mounts.h
+++ b/update_verifier/care_map.proto
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * 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.
@@ -14,15 +14,18 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+syntax = "proto3";
 
-struct MountedVolume;
+package recovery_update_verifier;
+option optimize_for = LITE_RUNTIME;
 
-bool scan_mounted_volumes();
+message CareMap {
+  message PartitionInfo {
+    string name = 1;
+    string ranges = 2;
+    string id = 3;
+    string fingerprint = 4;
+  }
 
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+  repeated PartitionInfo partitions = 1;
+}
diff --git a/update_verifier/care_map_generator.py b/update_verifier/care_map_generator.py
new file mode 100644
index 0000000..051d98d
--- /dev/null
+++ b/update_verifier/care_map_generator.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+#
+# 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.
+
+"""
+Parses a input care_map.txt in plain text format; converts it into the proto
+buf message; and writes the result to the output file.
+
+"""
+
+import argparse
+import logging
+import sys
+
+import care_map_pb2
+
+
+def GenerateCareMapProtoFromLegacyFormat(lines, fingerprint_enabled):
+  """Constructs a care map proto message from the lines of the input file."""
+
+  # Expected format of the legacy care_map.txt:
+  # system
+  # system's care_map ranges
+  # [system's fingerprint property id]
+  # [system's fingerprint]
+  # [vendor]
+  # [vendor's care_map ranges]
+  # [vendor's fingerprint property id]
+  # [vendor's fingerprint]
+  # ...
+
+  step = 4 if fingerprint_enabled else 2
+  assert len(lines) % step == 0, \
+      "line count must be multiple of {}: {}".format(step, len(lines))
+
+  care_map_proto = care_map_pb2.CareMap()
+  for index in range(0, len(lines), step):
+    info = care_map_proto.partitions.add()
+    info.name = lines[index]
+    info.ranges = lines[index + 1]
+    if fingerprint_enabled:
+      info.id = lines[index + 2]
+      info.fingerprint = lines[index + 3]
+    logging.info("Care map info: name %s, ranges %s, id %s, fingerprint %s",
+                 info.name, info.ranges, info.id, info.fingerprint)
+
+  return care_map_proto
+
+
+def ParseProtoMessage(message, fingerprint_enabled):
+  """Parses the care_map proto message and returns its text representation.
+  Args:
+    message: Care_map in protobuf format.
+    fingerprint_enabled: Input protobuf message contains the fields 'id' and
+        'fingerprint'.
+
+  Returns:
+     A string of the care_map information, similar to the care_map legacy
+     format.
+  """
+  care_map_proto = care_map_pb2.CareMap()
+  care_map_proto.MergeFromString(message)
+
+  info_list = []
+  for info in care_map_proto.partitions:
+    assert info.name, "partition name is required in care_map"
+    assert info.ranges, "source range is required in care_map"
+    info_list += [info.name, info.ranges]
+    if fingerprint_enabled:
+      assert info.id, "property id is required in care_map"
+      assert info.fingerprint, "fingerprint is required in care_map"
+      info_list += [info.id, info.fingerprint]
+
+  return '\n'.join(info_list)
+
+
+def main(argv):
+  parser = argparse.ArgumentParser(
+      description=__doc__,
+      formatter_class=argparse.RawDescriptionHelpFormatter)
+  parser.add_argument("input_care_map",
+                      help="Path to the legacy care_map file (or path to"
+                           " care_map in protobuf format if --parse_proto is"
+                           " specified).")
+  parser.add_argument("output_file",
+                      help="Path to output file to write the result.")
+  parser.add_argument("--no_fingerprint", action="store_false",
+                      dest="fingerprint_enabled",
+                      help="The 'id' and 'fingerprint' fields are disabled in"
+                           " the caremap.")
+  parser.add_argument("--parse_proto", "-p", action="store_true",
+                      help="Parses the input as proto message, and outputs"
+                           " the care_map in plain text.")
+  parser.add_argument("--verbose", "-v", action="store_true")
+
+  args = parser.parse_args(argv)
+
+  logging_format = '%(filename)s %(levelname)s: %(message)s'
+  logging.basicConfig(level=logging.INFO if args.verbose else logging.WARNING,
+                      format=logging_format)
+
+  with open(args.input_care_map, 'r') as input_care_map:
+    content = input_care_map.read()
+
+  if args.parse_proto:
+    result = ParseProtoMessage(content, args.fingerprint_enabled)
+  else:
+    care_map_proto = GenerateCareMapProtoFromLegacyFormat(
+        content.rstrip().splitlines(), args.fingerprint_enabled)
+    result = care_map_proto.SerializeToString()
+
+  with open(args.output_file, 'w') as output:
+    output.write(result)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
diff --git a/update_verifier/include/update_verifier/update_verifier.h b/update_verifier/include/update_verifier/update_verifier.h
index 16b394e..b00890e 100644
--- a/update_verifier/include/update_verifier/update_verifier.h
+++ b/update_verifier/include/update_verifier/update_verifier.h
@@ -16,9 +16,59 @@
 
 #pragma once
 
+#include <functional>
+#include <map>
 #include <string>
+#include <vector>
 
+#include "otautil/rangeset.h"
+
+// The update verifier performs verification upon the first boot to a new slot on A/B devices.
+// During the verification, it reads all the blocks in the care_map. And if a failure happens,
+// it rejects the current boot and triggers a fallback.
+
+// Note that update_verifier should be backward compatible to not reject care_map.txt from old
+// releases, which could otherwise fail to boot into the new release. For example, we've changed
+// the care_map format between N and O. An O update_verifier would fail to work with N care_map.txt.
+// This could be a result of sideloading an O OTA while the device having a pending N update.
 int update_verifier(int argc, char** argv);
 
-// Exposed for testing purpose.
-bool verify_image(const std::string& care_map_name);
+// The UpdateVerifier parses the content in the care map, and continues to verify the
+// partitions by reading the cared blocks if there's no format error in the file. Otherwise,
+// it should skip the verification to avoid bricking the device.
+class UpdateVerifier {
+ public:
+  UpdateVerifier();
+
+  // This function tries to process the care_map.pb as protobuf message; and falls back to use
+  // care_map.txt if the pb format file doesn't exist. If the parsing succeeds, put the result
+  // of the pair <partition_name, ranges> into the |partition_map_|.
+  bool ParseCareMap();
+
+  // Verifies the new boot by reading all the cared blocks for partitions in |partition_map_|.
+  bool VerifyPartitions();
+
+ private:
+  friend class UpdateVerifierTest;
+  // Parses the legacy care_map.txt in plain text format.
+  bool ParseCareMapPlainText(const std::string& content);
+
+  // Finds all the dm-enabled partitions, and returns a map of <partition_name, block_device>.
+  std::map<std::string, std::string> FindDmPartitions();
+
+  // Returns true if we successfully read the blocks in |ranges| of the |dm_block_device|.
+  bool ReadBlocks(const std::string partition_name, const std::string& dm_block_device,
+                  const RangeSet& ranges);
+
+  // Functions to override the care_map_prefix_ and property_reader_, used in test only.
+  void set_care_map_prefix(const std::string& prefix);
+  void set_property_reader(const std::function<std::string(const std::string&)>& property_reader);
+
+  std::map<std::string, RangeSet> partition_map_;
+  // The path to the care_map excluding the filename extension; default value:
+  // "/data/ota_package/care_map"
+  std::string care_map_prefix_;
+
+  // The function to read the device property; default value: android::base::GetProperty()
+  std::function<std::string(const std::string&)> property_reader_;
+};
diff --git a/update_verifier/update_verifier.cpp b/update_verifier/update_verifier.cpp
index 92d9313..d7cd061 100644
--- a/update_verifier/update_verifier.cpp
+++ b/update_verifier/update_verifier.cpp
@@ -15,24 +15,26 @@
  */
 
 /*
- * This program verifies the integrity of the partitions after an A/B OTA
- * update. It gets invoked by init, and will only perform the verification if
- * it's the first boot post an A/B OTA update.
+ * update_verifier verifies the integrity of the partitions after an A/B OTA update. It gets invoked
+ * by init, and will only perform the verification if it's the first boot post an A/B OTA update
+ * (https://source.android.com/devices/tech/ota/ab/#after_reboot).
  *
- * Update_verifier relies on dm-verity to capture any corruption on the partitions
- * being verified. And its behavior varies depending on the dm-verity mode.
- * Upon detection of failures:
+ * update_verifier relies on device-mapper-verity (dm-verity) to capture any corruption on the
+ * partitions being verified (https://source.android.com/security/verifiedboot). The verification
+ * will be skipped, if dm-verity is not enabled on the device.
+ *
+ * Upon detecting verification failures, the device will be rebooted, although the trigger of the
+ * reboot depends on the dm-verity mode.
  *   enforcing mode: dm-verity reboots the device
  *   eio mode: dm-verity fails the read and update_verifier reboots the device
  *   other mode: not supported and update_verifier reboots the device
  *
- * After a predefined number of failing boot attempts, the bootloader should
- * mark the slot as unbootable and stops trying. Other dm-verity modes (
- * for example, veritymode=EIO) are not accepted and simply lead to a
- * verification failure.
+ * All these reboots prevent the device from booting into a known corrupt state. If the device
+ * continuously fails to boot into the new slot, the bootloader should mark the slot as unbootable
+ * and trigger a fallback to the old slot.
  *
- * The current slot will be marked as having booted successfully if the
- * verifier reaches the end after the verification.
+ * The current slot will be marked as having booted successfully if the verifier reaches the end
+ * after the verification.
  */
 
 #include "update_verifier/update_verifier.h"
@@ -46,8 +48,6 @@
 
 #include <algorithm>
 #include <future>
-#include <string>
-#include <vector>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
@@ -58,13 +58,15 @@
 #include <android/hardware/boot/1.0/IBootControl.h>
 #include <cutils/android_reboot.h>
 
-#include "otautil/rangeset.h"
+#include "care_map.pb.h"
 
 using android::sp;
 using android::hardware::boot::V1_0::IBootControl;
 using android::hardware::boot::V1_0::BoolResult;
 using android::hardware::boot::V1_0::CommandResult;
 
+constexpr const char* kDefaultCareMapPrefix = "/data/ota_package/care_map";
+
 // Find directories in format of "/sys/block/dm-X".
 static int dm_name_filter(const dirent* de) {
   if (android::base::StartsWith(de->d_name, "dm-")) {
@@ -73,29 +75,29 @@
   return 0;
 }
 
-static bool read_blocks(const std::string& partition, const std::string& range_str) {
-  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 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.
+UpdateVerifier::UpdateVerifier()
+    : care_map_prefix_(kDefaultCareMapPrefix),
+      property_reader_([](const std::string& id) { return android::base::GetProperty(id, ""); }) {}
+
+// Iterate the content of "/sys/block/dm-X/dm/name" and find all the dm-wrapped block devices.
+// We will later read all the ("cared") blocks from "/dev/block/dm-X" to ensure the target
+// partition's integrity.
+std::map<std::string, std::string> UpdateVerifier::FindDmPartitions() {
   static constexpr auto DM_PATH_PREFIX = "/sys/block/";
   dirent** namelist;
   int n = scandir(DM_PATH_PREFIX, &namelist, dm_name_filter, alphasort);
   if (n == -1) {
     PLOG(ERROR) << "Failed to scan dir " << DM_PATH_PREFIX;
-    return false;
+    return {};
   }
   if (n == 0) {
-    LOG(ERROR) << "dm block device not found for " << partition;
-    return false;
+    LOG(ERROR) << "No dm block device found.";
+    return {};
   }
 
   static constexpr auto DM_PATH_SUFFIX = "/dm/name";
   static constexpr auto DEV_PATH = "/dev/block/";
-  std::string dm_block_device;
+  std::map<std::string, std::string> dm_block_devices;
   while (n--) {
     std::string path = DM_PATH_PREFIX + std::string(namelist[n]->d_name) + DM_PATH_SUFFIX;
     std::string content;
@@ -103,39 +105,22 @@
       PLOG(WARNING) << "Failed to read " << path;
     } else {
       std::string dm_block_name = android::base::Trim(content);
-#ifdef BOARD_AVB_ENABLE
       // AVB is using 'vroot' for the root block device but we're expecting 'system'.
       if (dm_block_name == "vroot") {
         dm_block_name = "system";
       }
-#endif
-      if (dm_block_name == partition) {
-        dm_block_device = DEV_PATH + std::string(namelist[n]->d_name);
-        while (n--) {
-          free(namelist[n]);
-        }
-        break;
-      }
+
+      dm_block_devices.emplace(dm_block_name, DEV_PATH + std::string(namelist[n]->d_name));
     }
     free(namelist[n]);
   }
   free(namelist);
 
-  if (dm_block_device.empty()) {
-    LOG(ERROR) << "Failed to find dm block device for " << partition;
-    return false;
-  }
+  return dm_block_devices;
+}
 
-  // For block range string, first integer 'count' equals 2 * total number of valid ranges,
-  // 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).
-  RangeSet ranges = RangeSet::Parse(range_str);
-  if (!ranges) {
-    LOG(ERROR) << "Error parsing RangeSet string " << range_str;
-    return false;
-  }
-
+bool UpdateVerifier::ReadBlocks(const std::string partition_name,
+                                const std::string& dm_block_device, const RangeSet& ranges) {
   // 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;
@@ -143,10 +128,10 @@
 
   std::vector<std::future<bool>> threads;
   for (const auto& group : groups) {
-    auto thread_func = [&group, &dm_block_device, &partition]() {
+    auto thread_func = [&group, &dm_block_device, &partition_name]() {
       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;
+        PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition_name;
         return false;
       }
 
@@ -154,9 +139,7 @@
       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;
+      for (const auto& [range_start, range_end] : group) {
         if (lseek64(fd.get(), static_cast<off64_t>(range_start) * kBlockSize, SEEK_SET) == -1) {
           PLOG(ERROR) << "lseek to " << range_start << " failed";
           return false;
@@ -189,47 +172,20 @@
   return ret;
 }
 
-// Returns true to indicate a passing verification (or the error should be ignored); Otherwise
-// returns false on fatal errors, where we should reject the current boot and trigger a fallback.
-// Note that update_verifier should be backward compatible to not reject care_map.txt from old
-// releases, which could otherwise fail to boot into the new release. For example, we've changed
-// the care_map format between N and O. An O update_verifier would fail to work with N
-// care_map.txt. This could be a result of sideloading an O OTA while the device having a pending N
-// update.
-bool verify_image(const std::string& care_map_name) {
-  android::base::unique_fd care_map_fd(TEMP_FAILURE_RETRY(open(care_map_name.c_str(), O_RDONLY)));
-  // If the device is flashed before the current boot, it may not have care_map.txt
-  // in /data/ota_package. To allow the device to continue booting in this situation,
-  // we should print a warning and skip the block verification.
-  if (care_map_fd.get() == -1) {
-    PLOG(WARNING) << "Failed to open " << care_map_name;
-    return true;
-  }
-  // 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.";
+bool UpdateVerifier::VerifyPartitions() {
+  auto dm_block_devices = FindDmPartitions();
+  if (dm_block_devices.empty()) {
+    LOG(ERROR) << "No dm-enabled block device is found.";
     return false;
   }
 
-  std::vector<std::string> lines;
-  lines = android::base::Split(android::base::Trim(file_content), "\n");
-  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 or 6 lines.";
-    return false;
-  }
-
-  for (size_t i = 0; i < lines.size(); i += 2) {
-    // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
-    // update_verifier (the last few metadata blocks can't be read via device mapper).
-    if (android::base::StartsWith(lines[i], "/dev/block/")) {
-      LOG(WARNING) << "Found legacy care_map.txt; skipped.";
-      return true;
+  for (const auto& [partition_name, ranges] : partition_map_) {
+    if (dm_block_devices.find(partition_name) == dm_block_devices.end()) {
+      LOG(ERROR) << "Failed to find dm block device for " << partition_name;
+      return false;
     }
-    if (!read_blocks(lines[i], lines[i+1])) {
+
+    if (!ReadBlocks(partition_name, dm_block_devices.at(partition_name), ranges)) {
       return false;
     }
   }
@@ -237,6 +193,133 @@
   return true;
 }
 
+bool UpdateVerifier::ParseCareMapPlainText(const std::string& content) {
+  // 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.
+  auto lines = android::base::Split(android::base::Trim(content), "\n");
+  if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
+    LOG(WARNING) << "Invalid lines in care_map: found " << lines.size()
+                 << " lines, expecting 2 or 4 or 6 lines.";
+    return false;
+  }
+
+  for (size_t i = 0; i < lines.size(); i += 2) {
+    const std::string& partition_name = lines[i];
+    const std::string& range_str = lines[i + 1];
+    // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
+    // update_verifier (the last few metadata blocks can't be read via device mapper).
+    if (android::base::StartsWith(partition_name, "/dev/block/")) {
+      LOG(WARNING) << "Found legacy care_map.txt; skipped.";
+      return false;
+    }
+
+    // For block range string, first integer 'count' equals 2 * total number of valid ranges,
+    // 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).
+    RangeSet ranges = RangeSet::Parse(range_str);
+    if (!ranges) {
+      LOG(WARNING) << "Error parsing RangeSet string " << range_str;
+      return false;
+    }
+
+    partition_map_.emplace(partition_name, ranges);
+  }
+
+  return true;
+}
+
+bool UpdateVerifier::ParseCareMap() {
+  partition_map_.clear();
+
+  std::string care_map_name = care_map_prefix_ + ".pb";
+  if (access(care_map_name.c_str(), R_OK) == -1) {
+    LOG(WARNING) << care_map_name
+                 << " doesn't exist, falling back to read the care_map in plain text format.";
+    care_map_name = care_map_prefix_ + ".txt";
+  }
+
+  android::base::unique_fd care_map_fd(TEMP_FAILURE_RETRY(open(care_map_name.c_str(), O_RDONLY)));
+  // If the device is flashed before the current boot, it may not have care_map.txt in
+  // /data/ota_package. To allow the device to continue booting in this situation, we should
+  // print a warning and skip the block verification.
+  if (care_map_fd.get() == -1) {
+    PLOG(WARNING) << "Failed to open " << care_map_name;
+    return false;
+  }
+
+  std::string file_content;
+  if (!android::base::ReadFdToString(care_map_fd.get(), &file_content)) {
+    PLOG(WARNING) << "Failed to read " << care_map_name;
+    return false;
+  }
+
+  if (file_content.empty()) {
+    LOG(WARNING) << "Unexpected empty care map";
+    return false;
+  }
+
+  if (android::base::EndsWith(care_map_name, ".txt")) {
+    return ParseCareMapPlainText(file_content);
+  }
+
+  recovery_update_verifier::CareMap care_map;
+  if (!care_map.ParseFromString(file_content)) {
+    LOG(WARNING) << "Failed to parse " << care_map_name << " in protobuf format.";
+    return false;
+  }
+
+  for (const auto& partition : care_map.partitions()) {
+    if (partition.name().empty()) {
+      LOG(WARNING) << "Unexpected empty partition name.";
+      return false;
+    }
+    if (partition.ranges().empty()) {
+      LOG(WARNING) << "Unexpected block ranges for partition " << partition.name();
+      return false;
+    }
+    RangeSet ranges = RangeSet::Parse(partition.ranges());
+    if (!ranges) {
+      LOG(WARNING) << "Error parsing RangeSet string " << partition.ranges();
+      return false;
+    }
+
+    // Continues to check other partitions if there is a fingerprint mismatch.
+    if (partition.id().empty() || partition.id() == "unknown") {
+      LOG(WARNING) << "Skip reading partition " << partition.name()
+                   << ": property_id is not provided to get fingerprint.";
+      continue;
+    }
+
+    std::string fingerprint = property_reader_(partition.id());
+    if (fingerprint != partition.fingerprint()) {
+      LOG(WARNING) << "Skip reading partition " << partition.name() << ": fingerprint "
+                   << fingerprint << " doesn't match the expected value "
+                   << partition.fingerprint();
+      continue;
+    }
+
+    partition_map_.emplace(partition.name(), ranges);
+  }
+
+  if (partition_map_.empty()) {
+    LOG(WARNING) << "No partition to verify";
+    return false;
+  }
+
+  return true;
+}
+
+void UpdateVerifier::set_care_map_prefix(const std::string& prefix) {
+  care_map_prefix_ = prefix;
+}
+
+void UpdateVerifier::set_property_reader(
+    const std::function<std::string(const std::string&)>& property_reader) {
+  property_reader_ = property_reader;
+}
+
 static int reboot_device() {
   if (android_reboot(ANDROID_RB_RESTART2, 0, nullptr) == -1) {
     LOG(ERROR) << "Failed to reboot.";
@@ -264,19 +347,13 @@
   if (is_successful == BoolResult::FALSE) {
     // The current slot has not booted successfully.
 
-#if defined(PRODUCT_SUPPORTS_VERITY) || defined(BOARD_AVB_ENABLE)
     bool skip_verification = false;
     std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
     if (verity_mode.empty()) {
-      // With AVB it's possible to disable verification entirely and
-      // in this case ro.boot.veritymode is empty.
-#if defined(BOARD_AVB_ENABLE)
-      LOG(WARNING) << "verification has been disabled; marking without verification.";
+      // Skip the verification if ro.boot.veritymode property is not set. This could be a result
+      // that device doesn't support dm-verity, or has disabled that.
+      LOG(WARNING) << "dm-verity not enabled; marking without verification.";
       skip_verification = true;
-#else
-      LOG(ERROR) << "Failed to get dm-verity mode.";
-      return reboot_device();
-#endif
     } else if (android::base::EqualsIgnoreCase(verity_mode, "eio")) {
       // We shouldn't see verity in EIO mode if the current slot hasn't booted successfully before.
       // Continue the verification until we fail to read some blocks.
@@ -285,20 +362,19 @@
       LOG(WARNING) << "dm-verity in disabled mode; marking without verification.";
       skip_verification = true;
     } else if (verity_mode != "enforcing") {
-      LOG(ERROR) << "Unexpected dm-verity mode : " << verity_mode << ", expecting enforcing.";
+      LOG(ERROR) << "Unexpected dm-verity mode: " << verity_mode << ", expecting enforcing.";
       return reboot_device();
     }
 
     if (!skip_verification) {
-      static constexpr auto CARE_MAP_FILE = "/data/ota_package/care_map.txt";
-      if (!verify_image(CARE_MAP_FILE)) {
+      UpdateVerifier verifier;
+      if (!verifier.ParseCareMap()) {
+        LOG(WARNING) << "Failed to parse the care map file, skipping verification";
+      } else if (!verifier.VerifyPartitions()) {
         LOG(ERROR) << "Failed to verify all blocks in care map file.";
         return reboot_device();
       }
     }
-#else
-    LOG(WARNING) << "dm-verity not enabled; marking without verification.";
-#endif
 
     CommandResult cr;
     module->markBootSuccessful([&cr](CommandResult result) { cr = result; });
diff --git a/updater/Android.bp b/updater/Android.bp
new file mode 100644
index 0000000..c95ec5e
--- /dev/null
+++ b/updater/Android.bp
@@ -0,0 +1,79 @@
+// 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.
+
+cc_defaults {
+    name: "libupdater_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    static_libs: [
+        "libapplypatch",
+        "libbootloader_message",
+        "libbspatch",
+        "libedify",
+        "libotautil",
+        "libext4_utils",
+        "libfec",
+        "libfec_rs",
+        "libverity_tree",
+        "libfs_mgr",
+        "libgtest_prod",
+        "liblog",
+        "libselinux",
+        "libsparse",
+        "libsquashfs_utils",
+        "libbrotli",
+        "libbz",
+        "libziparchive",
+        "libz",
+        "libbase",
+        "libcrypto",
+        "libcrypto_utils",
+        "libcutils",
+        "libutils",
+        "libtune2fs",
+
+        "libext2_com_err",
+        "libext2_blkid",
+        "libext2_quota",
+        "libext2_uuid",
+        "libext2_e2p",
+        "libext2fs",
+    ],
+}
+
+cc_library_static {
+    name: "libupdater",
+
+    defaults: [
+        "recovery_defaults",
+        "libupdater_defaults",
+    ],
+
+    srcs: [
+        "blockimg.cpp",
+        "commands.cpp",
+        "install.cpp",
+    ],
+
+    include_dirs: [
+        "external/e2fsprogs/misc",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+}
diff --git a/updater/Android.mk b/updater/Android.mk
index 6f334ee..78e32ba 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -24,59 +24,32 @@
 
 updater_common_static_libraries := \
     libapplypatch \
+    libbootloader_message \
     libbspatch \
     libedify \
-    libziparchive \
     libotautil \
-    libbootloader_message \
-    libutils \
-    libmounts \
-    libotafault \
     libext4_utils \
     libfec \
     libfec_rs \
+    libverity_tree \
     libfs_mgr \
+    libgtest_prod \
     liblog \
     libselinux \
     libsparse \
     libsquashfs_utils \
+    libbrotli \
     libbz \
+    libziparchive \
     libz \
     libbase \
     libcrypto \
     libcrypto_utils \
     libcutils \
+    libutils \
     libtune2fs \
-    libbrotli \
     $(tune2fs_static_libraries)
 
-# libupdater (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libupdater
-
-LOCAL_SRC_FILES := \
-    install.cpp \
-    blockimg.cpp
-
-LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/.. \
-    $(LOCAL_PATH)/include \
-    external/e2fsprogs/misc
-
-LOCAL_CFLAGS := \
-    -Wall \
-    -Werror
-
-LOCAL_EXPORT_C_INCLUDE_DIRS := \
-    $(LOCAL_PATH)/include
-
-LOCAL_STATIC_LIBRARIES := \
-    $(updater_common_static_libraries)
-
-include $(BUILD_STATIC_LIBRARY)
-
 # updater (static executable)
 # ===============================
 include $(CLEAR_VARS)
@@ -87,7 +60,6 @@
     updater.cpp
 
 LOCAL_C_INCLUDES := \
-    $(LOCAL_PATH)/.. \
     $(LOCAL_PATH)/include
 
 LOCAL_CFLAGS := \
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index e93196b..8388456 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -15,8 +15,8 @@
  */
 
 #include <ctype.h>
-#include <errno.h>
 #include <dirent.h>
+#include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>
 #include <linux/fs.h>
@@ -25,13 +25,12 @@
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/ioctl.h>
 #include <sys/stat.h>
 #include <sys/types.h>
 #include <sys/wait.h>
-#include <sys/ioctl.h>
 #include <time.h>
 #include <unistd.h>
-#include <fec/io.h>
 
 #include <functional>
 #include <limits>
@@ -47,16 +46,18 @@
 #include <android-base/unique_fd.h>
 #include <applypatch/applypatch.h>
 #include <brotli/decode.h>
+#include <fec/io.h>
 #include <openssl/sha.h>
 #include <private/android_filesystem_config.h>
+#include <verity/hash_tree_builder.h>
 #include <ziparchive/zip_archive.h>
 
 #include "edify/expr.h"
-#include "otafault/ota_io.h"
-#include "otautil/cache_location.h"
 #include "otautil/error_code.h"
+#include "otautil/paths.h"
 #include "otautil/print_sha1.h"
 #include "otautil/rangeset.h"
+#include "private/commands.h"
 #include "updater/install.h"
 #include "updater/updater.h"
 
@@ -74,7 +75,7 @@
 static std::unordered_map<std::string, RangeSet> stash_map;
 
 static void DeleteLastCommandFile() {
-  std::string last_command_file = CacheLocation::location().last_command_file();
+  const std::string& last_command_file = Paths::Get().last_command_file();
   if (unlink(last_command_file.c_str()) == -1 && errno != ENOENT) {
     PLOG(ERROR) << "Failed to unlink: " << last_command_file;
   }
@@ -82,8 +83,8 @@
 
 // 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();
+static bool ParseLastCommandFile(size_t* last_command_index) {
+  const std::string& last_command_file = Paths::Get().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) {
@@ -116,10 +117,24 @@
   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();
+static bool FsyncDir(const std::string& dirname) {
+  android::base::unique_fd dfd(TEMP_FAILURE_RETRY(open(dirname.c_str(), O_RDONLY | O_DIRECTORY)));
+  if (dfd == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
+    PLOG(ERROR) << "Failed to open " << dirname;
+    return false;
+  }
+  if (fsync(dfd) == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
+    PLOG(ERROR) << "Failed to fsync " << dirname;
+    return false;
+  }
+  return true;
+}
+
+// Update the last executed command index in the last_command_file.
+static bool UpdateLastCommandIndex(size_t command_index, const std::string& command_string) {
+  const std::string& last_command_file = Paths::Get().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(
@@ -144,61 +159,23 @@
     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;
+  if (!FsyncDir(android::base::Dirname(last_command_file))) {
     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) {
-        ssize_t r = TEMP_FAILURE_RETRY(ota_read(fd, data+so_far, size-so_far));
-        if (r == -1) {
-            failure_type = kFreadFailure;
-            PLOG(ERROR) << "read failed";
-            return -1;
-        } else if (r == 0) {
-            failure_type = kFreadFailure;
-            LOG(ERROR) << "read reached unexpected EOF.";
-            return -1;
-        }
-        so_far += r;
-    }
-    return 0;
-}
-
-static int read_all(int fd, std::vector<uint8_t>& buffer, size_t size) {
-    return read_all(fd, buffer.data(), size);
-}
-
-static int write_all(int fd, const uint8_t* data, size_t size) {
-    size_t written = 0;
-    while (written < size) {
-        ssize_t w = TEMP_FAILURE_RETRY(ota_write(fd, data+written, size-written));
-        if (w == -1) {
-            failure_type = kFwriteFailure;
-            PLOG(ERROR) << "write failed";
-            return -1;
-        }
-        written += w;
-    }
-
-    return 0;
-}
-
-static int write_all(int fd, const std::vector<uint8_t>& buffer, size_t size) {
-    return write_all(fd, buffer.data(), size);
+static bool SetPartitionUpdatedMarker(const std::string& marker) {
+  if (!android::base::WriteStringToFile("", marker)) {
+    PLOG(ERROR) << "Failed to write to marker file " << marker;
+    return false;
+  }
+  if (!FsyncDir(android::base::Dirname(marker))) {
+    return false;
+  }
+  LOG(INFO) << "Wrote partition updated marker to " << marker;
+  return true;
 }
 
 static bool discard_blocks(int fd, off64_t offset, uint64_t size) {
@@ -225,11 +202,10 @@
     return true;
 }
 
-static void allocate(size_t size, std::vector<uint8_t>& buffer) {
-    // if the buffer's big enough, reuse it.
-    if (size <= buffer.size()) return;
-
-    buffer.resize(size);
+static void allocate(size_t size, std::vector<uint8_t>* buffer) {
+  // If the buffer's big enough, reuse it.
+  if (size <= buffer->size()) return;
+  buffer->resize(size);
 }
 
 /**
@@ -274,7 +250,9 @@
         write_now = current_range_left_;
       }
 
-      if (write_all(fd_, data, write_now) == -1) {
+      if (!android::base::WriteFully(fd_, data, write_now)) {
+        failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+        PLOG(ERROR) << "Failed to write " << write_now << " bytes of data";
         break;
       }
 
@@ -483,15 +461,17 @@
   return nullptr;
 }
 
-static int ReadBlocks(const RangeSet& src, std::vector<uint8_t>& buffer, int fd) {
+static int ReadBlocks(const RangeSet& src, std::vector<uint8_t>* buffer, int fd) {
   size_t p = 0;
-  for (const auto& range : src) {
-    if (!check_lseek(fd, static_cast<off64_t>(range.first) * BLOCKSIZE, SEEK_SET)) {
+  for (const auto& [begin, end] : src) {
+    if (!check_lseek(fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
       return -1;
     }
 
-    size_t size = (range.second - range.first) * BLOCKSIZE;
-    if (read_all(fd, buffer.data() + p, size) == -1) {
+    size_t size = (end - begin) * BLOCKSIZE;
+    if (!android::base::ReadFully(fd, buffer->data() + p, size)) {
+      failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+      PLOG(ERROR) << "Failed to read " << size << " bytes of data";
       return -1;
     }
 
@@ -503,9 +483,9 @@
 
 static int WriteBlocks(const RangeSet& tgt, const std::vector<uint8_t>& buffer, int fd) {
   size_t written = 0;
-  for (const auto& range : tgt) {
-    off64_t offset = static_cast<off64_t>(range.first) * BLOCKSIZE;
-    size_t size = (range.second - range.first) * BLOCKSIZE;
+  for (const auto& [begin, end] : tgt) {
+    off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE;
+    size_t size = (end - begin) * BLOCKSIZE;
     if (!discard_blocks(fd, offset, size)) {
       return -1;
     }
@@ -514,7 +494,9 @@
       return -1;
     }
 
-    if (write_all(fd, buffer.data() + written, size) == -1) {
+    if (!android::base::WriteFully(fd, buffer.data() + written, size)) {
+      failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+      PLOG(ERROR) << "Failed to write " << size << " bytes of data";
       return -1;
     }
 
@@ -528,9 +510,8 @@
 struct CommandParameters {
     std::vector<std::string> tokens;
     size_t cpos;
-    int cmdindex;
-    const char* cmdname;
-    const char* cmdline;
+    std::string cmdname;
+    std::string cmdline;
     std::string freestash;
     std::string stashbase;
     bool canwrite;
@@ -644,43 +625,43 @@
   LOG(INFO) << "print hash in hex for source blocks in missing stash: " << id;
   const RangeSet& src = stash_map[id];
   std::vector<uint8_t> buffer(src.blocks() * BLOCKSIZE);
-  if (ReadBlocks(src, buffer, fd) == -1) {
-      LOG(ERROR) << "failed to read source blocks for stash: " << id;
-      return;
+  if (ReadBlocks(src, &buffer, fd) == -1) {
+    LOG(ERROR) << "failed to read source blocks for stash: " << id;
+    return;
   }
   PrintHashForCorruptedStashedBlocks(id, buffer, src);
 }
 
 static int VerifyBlocks(const std::string& expected, const std::vector<uint8_t>& buffer,
-        const size_t blocks, bool printerror) {
-    uint8_t digest[SHA_DIGEST_LENGTH];
-    const uint8_t* data = buffer.data();
+                        const size_t blocks, bool printerror) {
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  const uint8_t* data = buffer.data();
 
-    SHA1(data, blocks * BLOCKSIZE, digest);
+  SHA1(data, blocks * BLOCKSIZE, digest);
 
-    std::string hexdigest = print_sha1(digest);
+  std::string hexdigest = print_sha1(digest);
 
-    if (hexdigest != expected) {
-        if (printerror) {
-            LOG(ERROR) << "failed to verify blocks (expected " << expected << ", read "
-                       << hexdigest << ")";
-        }
-        return -1;
+  if (hexdigest != expected) {
+    if (printerror) {
+      LOG(ERROR) << "failed to verify blocks (expected " << expected << ", read " << hexdigest
+                 << ")";
     }
+    return -1;
+  }
 
-    return 0;
+  return 0;
 }
 
 static std::string GetStashFileName(const std::string& base, const std::string& id,
-        const std::string& postfix) {
-    if (base.empty()) {
-        return "";
-    }
-
-    std::string fn(CacheLocation::location().stash_directory_base());
-    fn += "/" + base + "/" + id + postfix;
-
-    return fn;
+                                    const std::string& postfix) {
+  if (base.empty()) {
+    return "";
+  }
+  std::string filename = Paths::Get().stash_directory_base() + "/" + base;
+  if (id.empty() && postfix.empty()) {
+    return filename;
+  }
+  return filename + "/" + id + postfix;
 }
 
 // Does a best effort enumeration of stash files. Ignores possible non-file items in the stash
@@ -733,8 +714,8 @@
   }
 }
 
-static int LoadStash(CommandParameters& params, const std::string& id, bool verify, size_t* blocks,
-                     std::vector<uint8_t>& buffer, bool printnoent) {
+static int LoadStash(const CommandParameters& params, const std::string& id, bool verify,
+                     std::vector<uint8_t>* buffer, bool printnoent) {
   // In verify mode, if source range_set was saved for the given hash, check contents in the source
   // blocks first. If the check fails, search for the stashed files on /cache as usual.
   if (!params.canwrite) {
@@ -746,20 +727,17 @@
         LOG(ERROR) << "failed to read source blocks in stash map.";
         return -1;
       }
-      if (VerifyBlocks(id, buffer, src.blocks(), true) != 0) {
+      if (VerifyBlocks(id, *buffer, src.blocks(), true) != 0) {
         LOG(ERROR) << "failed to verify loaded source blocks in stash map.";
-        PrintHashForCorruptedStashedBlocks(id, buffer, src);
+        if (!is_retry) {
+          PrintHashForCorruptedStashedBlocks(id, *buffer, src);
+        }
         return -1;
       }
       return 0;
     }
   }
 
-  size_t blockcount = 0;
-  if (!blocks) {
-    blocks = &blockcount;
-  }
-
   std::string fn = GetStashFileName(params.stashbase, id, "");
 
   struct stat sb;
@@ -778,28 +756,30 @@
     return -1;
   }
 
-  android::base::unique_fd fd(TEMP_FAILURE_RETRY(ota_open(fn.c_str(), O_RDONLY)));
+  android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(fn.c_str(), O_RDONLY)));
   if (fd == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
     PLOG(ERROR) << "open \"" << fn << "\" failed";
     return -1;
   }
 
   allocate(sb.st_size, buffer);
 
-  if (read_all(fd, buffer, sb.st_size) == -1) {
+  if (!android::base::ReadFully(fd, buffer->data(), sb.st_size)) {
+    failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+    PLOG(ERROR) << "Failed to read " << sb.st_size << " bytes of data";
     return -1;
   }
 
-  *blocks = sb.st_size / BLOCKSIZE;
-
-  if (verify && VerifyBlocks(id, buffer, *blocks, true) != 0) {
+  size_t blocks = sb.st_size / BLOCKSIZE;
+  if (verify && VerifyBlocks(id, *buffer, blocks, true) != 0) {
     LOG(ERROR) << "unexpected contents in " << fn;
     if (stash_map.find(id) == stash_map.end()) {
       LOG(ERROR) << "failed to find source blocks number for stash " << id
                  << " when executing command: " << params.cmdname;
     } else {
       const RangeSet& src = stash_map[id];
-      PrintHashForCorruptedStashedBlocks(id, buffer, src);
+      PrintHashForCorruptedStashedBlocks(id, *buffer, src);
     }
     DeleteFile(fn);
     return -1;
@@ -809,109 +789,90 @@
 }
 
 static int WriteStash(const std::string& base, const std::string& id, int blocks,
-                      std::vector<uint8_t>& buffer, bool checkspace, bool* exists) {
-    if (base.empty()) {
-        return -1;
+                      const std::vector<uint8_t>& buffer, bool checkspace, bool* exists) {
+  if (base.empty()) {
+    return -1;
+  }
+
+  if (checkspace && !CheckAndFreeSpaceOnCache(blocks * BLOCKSIZE)) {
+    LOG(ERROR) << "not enough space to write stash";
+    return -1;
+  }
+
+  std::string fn = GetStashFileName(base, id, ".partial");
+  std::string cn = GetStashFileName(base, id, "");
+
+  if (exists) {
+    struct stat sb;
+    int res = stat(cn.c_str(), &sb);
+
+    if (res == 0) {
+      // The file already exists and since the name is the hash of the contents,
+      // it's safe to assume the contents are identical (accidental hash collisions
+      // are unlikely)
+      LOG(INFO) << " skipping " << blocks << " existing blocks in " << cn;
+      *exists = true;
+      return 0;
     }
 
-    if (checkspace && CacheSizeCheck(blocks * BLOCKSIZE) != 0) {
-        LOG(ERROR) << "not enough space to write stash";
-        return -1;
-    }
+    *exists = false;
+  }
 
-    std::string fn = GetStashFileName(base, id, ".partial");
-    std::string cn = GetStashFileName(base, id, "");
+  LOG(INFO) << " writing " << blocks << " blocks to " << cn;
 
-    if (exists) {
-        struct stat sb;
-        int res = stat(cn.c_str(), &sb);
+  android::base::unique_fd fd(
+      TEMP_FAILURE_RETRY(open(fn.c_str(), O_WRONLY | O_CREAT | O_TRUNC, STASH_FILE_MODE)));
+  if (fd == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
+    PLOG(ERROR) << "failed to create \"" << fn << "\"";
+    return -1;
+  }
 
-        if (res == 0) {
-            // The file already exists and since the name is the hash of the contents,
-            // it's safe to assume the contents are identical (accidental hash collisions
-            // are unlikely)
-            LOG(INFO) << " skipping " << blocks << " existing blocks in " << cn;
-            *exists = true;
-            return 0;
-        }
+  if (fchown(fd, AID_SYSTEM, AID_SYSTEM) != 0) {  // system user
+    PLOG(ERROR) << "failed to chown \"" << fn << "\"";
+    return -1;
+  }
 
-        *exists = false;
-    }
+  if (!android::base::WriteFully(fd, buffer.data(), blocks * BLOCKSIZE)) {
+    failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+    PLOG(ERROR) << "Failed to write " << blocks * BLOCKSIZE << " bytes of data";
+    return -1;
+  }
 
-    LOG(INFO) << " writing " << blocks << " blocks to " << cn;
+  if (fsync(fd) == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
+    PLOG(ERROR) << "fsync \"" << fn << "\" failed";
+    return -1;
+  }
 
-    android::base::unique_fd fd(
-        TEMP_FAILURE_RETRY(ota_open(fn.c_str(), O_WRONLY | O_CREAT | O_TRUNC, STASH_FILE_MODE)));
-    if (fd == -1) {
-        PLOG(ERROR) << "failed to create \"" << fn << "\"";
-        return -1;
-    }
+  if (rename(fn.c_str(), cn.c_str()) == -1) {
+    PLOG(ERROR) << "rename(\"" << fn << "\", \"" << cn << "\") failed";
+    return -1;
+  }
 
-    if (fchown(fd, AID_SYSTEM, AID_SYSTEM) != 0) {  // system user
-        PLOG(ERROR) << "failed to chown \"" << fn << "\"";
-        return -1;
-    }
+  std::string dname = GetStashFileName(base, "", "");
+  if (!FsyncDir(dname)) {
+    return -1;
+  }
 
-    if (write_all(fd, buffer, blocks * BLOCKSIZE) == -1) {
-        return -1;
-    }
-
-    if (ota_fsync(fd) == -1) {
-        failure_type = kFsyncFailure;
-        PLOG(ERROR) << "fsync \"" << fn << "\" failed";
-        return -1;
-    }
-
-    if (rename(fn.c_str(), cn.c_str()) == -1) {
-        PLOG(ERROR) << "rename(\"" << fn << "\", \"" << cn << "\") failed";
-        return -1;
-    }
-
-    std::string dname = GetStashFileName(base, "", "");
-    android::base::unique_fd dfd(TEMP_FAILURE_RETRY(ota_open(dname.c_str(),
-                                                             O_RDONLY | O_DIRECTORY)));
-    if (dfd == -1) {
-        failure_type = kFileOpenFailure;
-        PLOG(ERROR) << "failed to open \"" << dname << "\" failed";
-        return -1;
-    }
-
-    if (ota_fsync(dfd) == -1) {
-        failure_type = kFsyncFailure;
-        PLOG(ERROR) << "fsync \"" << dname << "\" failed";
-        return -1;
-    }
-
-    return 0;
+  return 0;
 }
 
 // Creates a directory for storing stash files and checks if the /cache partition
 // hash enough space for the expected amount of blocks we need to store. Returns
 // >0 if we created the directory, zero if it existed already, and <0 of failure.
-
-static int CreateStash(State* state, size_t maxblocks, const std::string& blockdev,
-                       std::string& base) {
-  if (blockdev.empty()) {
-    return -1;
-  }
-
-  // Stash directory should be different for each partition to avoid conflicts
-  // when updating multiple partitions at the same time, so we use the hash of
-  // the block device name as the base directory
-  uint8_t digest[SHA_DIGEST_LENGTH];
-  SHA1(reinterpret_cast<const uint8_t*>(blockdev.data()), blockdev.size(), digest);
-  base = print_sha1(digest);
-
+static int CreateStash(State* state, size_t maxblocks, const std::string& base) {
   std::string dirname = GetStashFileName(base, "", "");
   struct stat sb;
   int res = stat(dirname.c_str(), &sb);
-  size_t max_stash_size = maxblocks * BLOCKSIZE;
-
   if (res == -1 && errno != ENOENT) {
     ErrorAbort(state, kStashCreationFailure, "stat \"%s\" failed: %s", dirname.c_str(),
                strerror(errno));
     return -1;
-  } else if (res != 0) {
+  }
+
+  size_t max_stash_size = maxblocks * BLOCKSIZE;
+  if (res == -1) {
     LOG(INFO) << "creating stash " << dirname;
     res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE);
 
@@ -927,7 +888,7 @@
       return -1;
     }
 
-    if (CacheSizeCheck(max_stash_size) != 0) {
+    if (!CheckAndFreeSpaceOnCache(max_stash_size)) {
       ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu needed)",
                  max_stash_size);
       return -1;
@@ -959,7 +920,7 @@
 
   if (max_stash_size > existing) {
     size_t needed = max_stash_size - existing;
-    if (CacheSizeCheck(needed) != 0) {
+    if (!CheckAndFreeSpaceOnCache(needed)) {
       ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu more needed)",
                  needed);
       return -1;
@@ -1023,7 +984,7 @@
     return -1;
   }
 
-  allocate(*src_blocks * BLOCKSIZE, params.buffer);
+  allocate(*src_blocks * BLOCKSIZE, &params.buffer);
 
   // "-" or <src_range> [<src_loc>]
   if (params.tokens[params.cpos] == "-") {
@@ -1034,7 +995,7 @@
     CHECK(static_cast<bool>(src));
     *overlap = src.Overlaps(tgt);
 
-    if (ReadBlocks(src, params.buffer, params.fd) == -1) {
+    if (ReadBlocks(src, &params.buffer, params.fd) == -1) {
       return -1;
     }
 
@@ -1059,7 +1020,7 @@
     }
 
     std::vector<uint8_t> stash;
-    if (LoadStash(params, tokens[0], false, nullptr, stash, true) == -1) {
+    if (LoadStash(params, tokens[0], false, &stash, true) == -1) {
       // These source blocks will fail verification if used later, but we
       // will let the caller decide if this is a fatal failure
       LOG(ERROR) << "failed to load stash " << tokens[0];
@@ -1100,10 +1061,9 @@
  *
  * If the return value is 0, source blocks have expected content and the command can be performed.
  */
-static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet& tgt, size_t* src_blocks,
-                              bool onehash, bool* overlap) {
+static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet* tgt, size_t* src_blocks,
+                              bool onehash) {
   CHECK(src_blocks != nullptr);
-  CHECK(overlap != nullptr);
 
   if (params.cpos >= params.tokens.size()) {
     LOG(ERROR) << "missing source hash";
@@ -1131,29 +1091,30 @@
   }
 
   // <tgt_range>
-  tgt = RangeSet::Parse(params.tokens[params.cpos++]);
-  CHECK(static_cast<bool>(tgt));
+  *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) {
+  std::vector<uint8_t> tgtbuffer(tgt->blocks() * BLOCKSIZE);
+  if (ReadBlocks(*tgt, &tgtbuffer, params.fd) == -1) {
     return -1;
   }
 
   // Return now if target blocks already have expected content.
-  if (VerifyBlocks(tgthash, tgtbuffer, tgt.blocks(), false) == 0) {
+  if (VerifyBlocks(tgthash, tgtbuffer, tgt->blocks(), false) == 0) {
     return 1;
   }
 
   // Load source blocks.
-  if (LoadSourceBlocks(params, tgt, src_blocks, overlap) == -1) {
+  bool overlap = false;
+  if (LoadSourceBlocks(params, *tgt, src_blocks, &overlap) == -1) {
     return -1;
   }
 
   if (VerifyBlocks(srchash, params.buffer, *src_blocks, true) == 0) {
-    // If source and target blocks overlap, stash the source blocks so we can
-    // resume from possible write errors. In verify mode, we can skip stashing
-    // because the source blocks won't be overwritten.
-    if (*overlap && params.canwrite) {
+    // If source and target blocks overlap, stash the source blocks so we can resume from possible
+    // write errors. In verify mode, we can skip stashing because the source blocks won't be
+    // overwritten.
+    if (overlap && params.canwrite) {
       LOG(INFO) << "stashing " << *src_blocks << " overlapping blocks to " << srchash;
 
       bool stash_exists = false;
@@ -1163,10 +1124,6 @@
         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) {
@@ -1178,7 +1135,7 @@
     return 0;
   }
 
-  if (*overlap && LoadStash(params, srchash, true, nullptr, params.buffer, true) == 0) {
+  if (overlap && LoadStash(params, srchash, true, &params.buffer, true) == 0) {
     // Overlapping source blocks were previously stashed, command can proceed. We are recovering
     // from an interrupted command, so we don't know if the stash can safely be deleted after this
     // command.
@@ -1196,9 +1153,8 @@
 
 static int PerformCommandMove(CommandParameters& params) {
   size_t blocks = 0;
-  bool overlap = false;
   RangeSet tgt;
-  int status = LoadSrcTgtVersion3(params, tgt, &blocks, true, &overlap);
+  int status = LoadSrcTgtVersion3(params, &tgt, &blocks, true);
 
   if (status == -1) {
     LOG(ERROR) << "failed to read blocks for move";
@@ -1244,8 +1200,7 @@
   }
 
   const std::string& id = params.tokens[params.cpos++];
-  size_t blocks = 0;
-  if (LoadStash(params, id, true, &blocks, params.buffer, false) == 0) {
+  if (LoadStash(params, id, true, &params.buffer, false) == 0) {
     // Stash file already exists and has expected contents. Do not read from source again, as the
     // source may have been already overwritten during a previous attempt.
     return 0;
@@ -1254,11 +1209,11 @@
   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) {
+  size_t blocks = src.blocks();
+  allocate(blocks * BLOCKSIZE, &params.buffer);
+  if (ReadBlocks(src, &params.buffer, params.fd) == -1) {
     return -1;
   }
-  blocks = src.blocks();
   stash_map[id] = src;
 
   if (VerifyBlocks(id, params.buffer, blocks, true) != 0) {
@@ -1277,10 +1232,6 @@
   LOG(INFO) << "stashing " << blocks << " blocks to " << id;
   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;
@@ -1314,13 +1265,13 @@
 
   LOG(INFO) << "  zeroing " << tgt.blocks() << " blocks";
 
-  allocate(BLOCKSIZE, params.buffer);
+  allocate(BLOCKSIZE, &params.buffer);
   memset(params.buffer.data(), 0, BLOCKSIZE);
 
   if (params.canwrite) {
-    for (const auto& range : tgt) {
-      off64_t offset = static_cast<off64_t>(range.first) * BLOCKSIZE;
-      size_t size = (range.second - range.first) * BLOCKSIZE;
+    for (const auto& [begin, end] : tgt) {
+      off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE;
+      size_t size = (end - begin) * BLOCKSIZE;
       if (!discard_blocks(params.fd, offset, size)) {
         return -1;
       }
@@ -1329,8 +1280,10 @@
         return -1;
       }
 
-      for (size_t j = range.first; j < range.second; ++j) {
-        if (write_all(params.fd, params.buffer, BLOCKSIZE) == -1) {
+      for (size_t j = begin; j < end; ++j) {
+        if (!android::base::WriteFully(params.fd, params.buffer.data(), BLOCKSIZE)) {
+          failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+          PLOG(ERROR) << "Failed to write " << BLOCKSIZE << " bytes of data";
           return -1;
         }
       }
@@ -1401,8 +1354,7 @@
 
   RangeSet tgt;
   size_t blocks = 0;
-  bool overlap = false;
-  int status = LoadSrcTgtVersion3(params, tgt, &blocks, false, &overlap);
+  int status = LoadSrcTgtVersion3(params, &tgt, &blocks, false);
 
   if (status == -1) {
     LOG(ERROR) << "failed to read blocks for diff";
@@ -1422,14 +1374,15 @@
     if (status == 0) {
       LOG(INFO) << "patching " << blocks << " blocks to " << tgt.blocks();
       Value patch_value(
-          VAL_BLOB, std::string(reinterpret_cast<const char*>(params.patch_start + offset), len));
+          Value::Type::BLOB,
+          std::string(reinterpret_cast<const char*>(params.patch_start + offset), len));
 
       RangeSinkWriter writer(params.fd, tgt);
       if (params.cmdname[0] == 'i') {  // imgdiff
         if (ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value,
                             std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
                                       std::placeholders::_2),
-                            nullptr, nullptr) != 0) {
+                            nullptr) != 0) {
           LOG(ERROR) << "Failed to apply image patch.";
           failure_type = kPatchApplicationFailure;
           return -1;
@@ -1437,8 +1390,7 @@
       } else {
         if (ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value, 0,
                              std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
-                                       std::placeholders::_2),
-                             nullptr) != 0) {
+                                       std::placeholders::_2)) != 0) {
           LOG(ERROR) << "Failed to apply bsdiff patch.";
           failure_type = kPatchApplicationFailure;
           return -1;
@@ -1492,12 +1444,12 @@
   if (params.canwrite) {
     LOG(INFO) << " erasing " << tgt.blocks() << " blocks";
 
-    for (const auto& range : tgt) {
+    for (const auto& [begin, end] : tgt) {
       uint64_t blocks[2];
       // offset in bytes
-      blocks[0] = range.first * static_cast<uint64_t>(BLOCKSIZE);
+      blocks[0] = begin * static_cast<uint64_t>(BLOCKSIZE);
       // length in bytes
-      blocks[1] = (range.second - range.first) * static_cast<uint64_t>(BLOCKSIZE);
+      blocks[1] = (end - begin) * static_cast<uint64_t>(BLOCKSIZE);
 
       if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) {
         PLOG(ERROR) << "BLKDISCARD ioctl failed";
@@ -1509,24 +1461,120 @@
   return 0;
 }
 
-// Definitions for transfer list command functions
-typedef int (*CommandFunction)(CommandParameters&);
+static int PerformCommandAbort(CommandParameters&) {
+  LOG(INFO) << "Aborting as instructed";
+  return -1;
+}
 
-struct Command {
-    const char* name;
-    CommandFunction f;
-};
+// Computes the hash_tree bytes based on the parameters, checks if the root hash of the tree
+// matches the expected hash and writes the result to the specified range on the block_device.
+// Hash_tree computation arguments:
+//   hash_tree_ranges
+//   source_ranges
+//   hash_algorithm
+//   salt_hex
+//   root_hash
+static int PerformCommandComputeHashTree(CommandParameters& params) {
+  if (params.cpos + 5 != params.tokens.size()) {
+    LOG(ERROR) << "Invaild arguments count in hash computation " << params.cmdline;
+    return -1;
+  }
 
-// args:
-//    - block device (or file) to modify in-place
-//    - transfer list (blob)
-//    - new data stream (filename within package.zip)
-//    - patch stream (filename within package.zip, must be uncompressed)
+  // Expects the hash_tree data to be contiguous.
+  RangeSet hash_tree_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
+  if (!hash_tree_ranges || hash_tree_ranges.size() != 1) {
+    LOG(ERROR) << "Invalid hash tree ranges in " << params.cmdline;
+    return -1;
+  }
+
+  RangeSet source_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
+  if (!source_ranges) {
+    LOG(ERROR) << "Invalid source ranges in " << params.cmdline;
+    return -1;
+  }
+
+  auto hash_function = HashTreeBuilder::HashFunction(params.tokens[params.cpos++]);
+  if (hash_function == nullptr) {
+    LOG(ERROR) << "Invalid hash algorithm in " << params.cmdline;
+    return -1;
+  }
+
+  std::vector<unsigned char> salt;
+  std::string salt_hex = params.tokens[params.cpos++];
+  if (salt_hex.empty() || !HashTreeBuilder::ParseBytesArrayFromString(salt_hex, &salt)) {
+    LOG(ERROR) << "Failed to parse salt in " << params.cmdline;
+    return -1;
+  }
+
+  std::string expected_root_hash = params.tokens[params.cpos++];
+  if (expected_root_hash.empty()) {
+    LOG(ERROR) << "Invalid root hash in " << params.cmdline;
+    return -1;
+  }
+
+  // Starts the hash_tree computation.
+  HashTreeBuilder builder(BLOCKSIZE, hash_function);
+  if (!builder.Initialize(source_ranges.blocks() * BLOCKSIZE, salt)) {
+    LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString()
+               << ", salt " << salt_hex;
+    return -1;
+  }
+
+  // Iterates through every block in the source_ranges and updates the hash tree structure
+  // accordingly.
+  for (const auto& [begin, end] : source_ranges) {
+    uint8_t buffer[BLOCKSIZE];
+    if (!check_lseek(params.fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
+      PLOG(ERROR) << "Failed to seek to block: " << begin;
+      return -1;
+    }
+
+    for (size_t i = begin; i < end; i++) {
+      if (!android::base::ReadFully(params.fd, buffer, BLOCKSIZE)) {
+        failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+        LOG(ERROR) << "Failed to read data in " << begin << ":" << end;
+        return -1;
+      }
+
+      if (!builder.Update(reinterpret_cast<unsigned char*>(buffer), BLOCKSIZE)) {
+        LOG(ERROR) << "Failed to update hash tree builder";
+        return -1;
+      }
+    }
+  }
+
+  if (!builder.BuildHashTree()) {
+    LOG(ERROR) << "Failed to build hash tree";
+    return -1;
+  }
+
+  std::string root_hash_hex = HashTreeBuilder::BytesArrayToString(builder.root_hash());
+  if (root_hash_hex != expected_root_hash) {
+    LOG(ERROR) << "Root hash of the verity hash tree doesn't match the expected value. Expected: "
+               << expected_root_hash << ", actual: " << root_hash_hex;
+    return -1;
+  }
+
+  uint64_t write_offset = static_cast<uint64_t>(hash_tree_ranges.GetBlockNumber(0)) * BLOCKSIZE;
+  if (params.canwrite && !builder.WriteHashTreeToFd(params.fd, write_offset)) {
+    LOG(ERROR) << "Failed to write hash tree to output";
+    return -1;
+  }
+
+  // TODO(xunchang) validates the written bytes
+
+  return 0;
+}
+
+using CommandFunction = std::function<int(CommandParameters&)>;
+
+using CommandMap = std::unordered_map<Command::Type, CommandFunction>;
 
 static Value* PerformBlockImageUpdate(const char* name, State* state,
                                       const std::vector<std::unique_ptr<Expr>>& argv,
-                                      const Command* commands, size_t cmdcount, bool dryrun) {
+                                      const CommandMap& command_map, bool dryrun) {
   CommandParameters params = {};
+  stash_map.clear();
   params.canwrite = !dryrun;
 
   LOG(INFO) << "performing " << (dryrun ? "verification" : "update");
@@ -1545,24 +1593,29 @@
     return nullptr;
   }
 
+  // args:
+  //   - block device (or file) to modify in-place
+  //   - transfer list (blob)
+  //   - new data stream (filename within package.zip)
+  //   - patch stream (filename within package.zip, must be uncompressed)
   const std::unique_ptr<Value>& blockdev_filename = args[0];
   const std::unique_ptr<Value>& transfer_list_value = args[1];
   const std::unique_ptr<Value>& new_data_fn = args[2];
   const std::unique_ptr<Value>& patch_data_fn = args[3];
 
-  if (blockdev_filename->type != VAL_STRING) {
+  if (blockdev_filename->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name);
     return StringValue("");
   }
-  if (transfer_list_value->type != VAL_BLOB) {
+  if (transfer_list_value->type != Value::Type::BLOB) {
     ErrorAbort(state, kArgsParsingFailure, "transfer_list argument to %s must be blob", name);
     return StringValue("");
   }
-  if (new_data_fn->type != VAL_STRING) {
+  if (new_data_fn->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "new_data_fn argument to %s must be string", name);
     return StringValue("");
   }
-  if (patch_data_fn->type != VAL_STRING) {
+  if (patch_data_fn->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "patch_data_fn argument to %s must be string", name);
     return StringValue("");
   }
@@ -1594,38 +1647,46 @@
     return StringValue("");
   }
 
-  params.fd.reset(TEMP_FAILURE_RETRY(ota_open(blockdev_filename->data.c_str(), O_RDWR)));
+  params.fd.reset(TEMP_FAILURE_RETRY(open(blockdev_filename->data.c_str(), O_RDWR)));
   if (params.fd == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
     PLOG(ERROR) << "open \"" << blockdev_filename->data << "\" failed";
     return StringValue("");
   }
 
-  if (params.canwrite) {
-    params.nti.za = za;
-    params.nti.entry = new_entry;
-    params.nti.brotli_compressed = android::base::EndsWith(new_data_fn->data, ".br");
-    if (params.nti.brotli_compressed) {
-      // Initialize brotli decoder state.
-      params.nti.brotli_decoder_state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
+  // Stash directory should be different for each partition to avoid conflicts when updating
+  // multiple partitions at the same time, so we use the hash of the block device name as the base
+  // directory.
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  SHA1(reinterpret_cast<const uint8_t*>(blockdev_filename->data.data()),
+       blockdev_filename->data.size(), digest);
+  params.stashbase = print_sha1(digest);
+
+  // Possibly do return early on retry, by checking the marker. If the update on this partition has
+  // been finished (but interrupted at a later point), there could be leftover on /cache that would
+  // fail the no-op retry.
+  std::string updated_marker = GetStashFileName(params.stashbase + ".UPDATED", "", "");
+  if (is_retry) {
+    struct stat sb;
+    int result = stat(updated_marker.c_str(), &sb);
+    if (result == 0) {
+      LOG(INFO) << "Skipping already updated partition " << blockdev_filename->data
+                << " based on marker";
+      return StringValue("t");
     }
-    params.nti.receiver_available = true;
-
-    pthread_mutex_init(&params.nti.mu, nullptr);
-    pthread_cond_init(&params.nti.cv, nullptr);
-    pthread_attr_t attr;
-    pthread_attr_init(&attr);
-    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
-
-    int error = pthread_create(&params.thread, &attr, unzip_new_data, &params.nti);
-    if (error != 0) {
-      PLOG(ERROR) << "pthread_create failed";
+  } else {
+    // Delete the obsolete marker if any.
+    std::string err;
+    if (!android::base::RemoveFileIfExists(updated_marker, &err)) {
+      LOG(ERROR) << "Failed to remove partition updated marker " << updated_marker << ": " << err;
       return StringValue("");
     }
   }
 
+  static constexpr size_t kTransferListHeaderLines = 4;
   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]",
+  if (lines.size() < kTransferListHeaderLines) {
+    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]",
                lines.size());
     return StringValue("");
   }
@@ -1649,13 +1710,6 @@
     return StringValue("t");
   }
 
-  size_t start = 2;
-  if (lines.size() < 4) {
-    ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]",
-               lines.size());
-    return StringValue("");
-  }
-
   // Third line is how many stash entries are needed simultaneously.
   LOG(INFO) << "maximum stash entries " << lines[2];
 
@@ -1667,15 +1721,38 @@
     return StringValue("");
   }
 
-  int res = CreateStash(state, stash_max_blocks, blockdev_filename->data, params.stashbase);
+  int res = CreateStash(state, stash_max_blocks, params.stashbase);
   if (res == -1) {
     return StringValue("");
   }
-
   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.
+  // Set up the new data writer.
+  if (params.canwrite) {
+    params.nti.za = za;
+    params.nti.entry = new_entry;
+    params.nti.brotli_compressed = android::base::EndsWith(new_data_fn->data, ".br");
+    if (params.nti.brotli_compressed) {
+      // Initialize brotli decoder state.
+      params.nti.brotli_decoder_state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
+    }
+    params.nti.receiver_available = true;
+
+    pthread_mutex_init(&params.nti.mu, nullptr);
+    pthread_cond_init(&params.nti.cv, nullptr);
+    pthread_attr_t attr;
+    pthread_attr_init(&attr);
+    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+
+    int error = pthread_create(&params.thread, &attr, unzip_new_data, &params.nti);
+    if (error != 0) {
+      LOG(ERROR) << "pthread_create failed: " << strerror(error);
+      return StringValue("");
+    }
+  }
+
+  // When performing an update, save the index and cmdline of the current command into the
+  // last_command_file.
   // 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
@@ -1684,90 +1761,86 @@
   //   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;
+  bool skip_executed_command = true;
+  size_t 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
-  std::unordered_map<std::string, const Command*> cmd_map;
-  for (size_t i = 0; i < cmdcount; ++i) {
-    if (cmd_map.find(commands[i].name) != cmd_map.end()) {
-      LOG(ERROR) << "Error: command [" << commands[i].name << "] already exists in the cmd map.";
-      return StringValue(strdup(""));
-    }
-    cmd_map[commands[i].name] = &commands[i];
+    // We failed to parse the last command. Disallow skipping executed commands.
+    skip_executed_command = false;
   }
 
   int rc = -1;
 
   // Subsequent lines are all individual transfer commands
-  for (size_t i = start; i < lines.size(); i++) {
+  for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) {
     const std::string& line = lines[i];
     if (line.empty()) continue;
 
+    size_t cmdindex = i - kTransferListHeaderLines;
     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.cmdname = params.tokens[params.cpos++];
+    params.cmdline = line;
     params.target_verified = false;
 
-    if (cmd_map.find(params.cmdname) == cmd_map.end()) {
+    Command::Type cmd_type = Command::ParseType(params.cmdname);
+    if (cmd_type == Command::Type::LAST) {
       LOG(ERROR) << "unexpected command [" << params.cmdname << "]";
       goto pbiudone;
     }
 
-    const Command* cmd = cmd_map[params.cmdname];
+    const CommandFunction& performer = command_map.at(cmd_type);
 
     // Skip the command if we explicitly set the corresponding function pointer to nullptr, e.g.
     // "erase" during block_image_verify.
-    if (cmd->f == nullptr) {
+    if (performer == 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
+    // Skip all commands before the saved last command index when resuming an update, except for
+    // "new" command. Because new commands read in the data sequentially.
+    if (params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index &&
+        cmd_type != Command::Type::NEW) {
+      LOG(INFO) << "Skipping already executed command: " << cmdindex
                 << ", last executed command for previous update: " << saved_last_command_index;
       continue;
     }
 
-    if (cmd->f(params) == -1) {
+    if (performer(params) == -1) {
       LOG(ERROR) << "failed to execute command [" << line << "]";
+      if (cmd_type == Command::Type::COMPUTE_HASH_TREE && failure_type == kNoCause) {
+        failure_type = kHashTreeComputationFailure;
+      }
       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) {
+    // 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 && skip_executed_command && 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") &&
+      if ((cmd_type == Command::Type::MOVE || cmd_type == Command::Type::BSDIFF ||
+           cmd_type == Command::Type::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;
+        skip_executed_command = false;
         DeleteLastCommandFile();
       }
     }
+
     if (params.canwrite) {
-      if (ota_fsync(params.fd) == -1) {
-        failure_type = kFsyncFailure;
+      if (fsync(params.fd) == -1) {
+        failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
         PLOG(ERROR) << "fsync failed";
         goto pbiudone;
       }
+
+      if (!UpdateLastCommandIndex(cmdindex, params.cmdline)) {
+        LOG(WARNING) << "Failed to update the last command file.";
+      }
+
       fprintf(cmd_pipe, "set_progress %.4f\n", static_cast<double>(params.written) / total_blocks);
       fflush(cmd_pipe);
     }
@@ -1804,6 +1877,13 @@
       // to complete the update later.
       DeleteStash(params.stashbase);
       DeleteLastCommandFile();
+
+      // Create a marker on /cache partition, which allows skipping the update on this partition on
+      // retry. The marker will be removed once booting into normal boot, or before starting next
+      // fresh install.
+      if (!SetPartitionUpdatedMarker(updated_marker)) {
+        LOG(WARNING) << "Failed to set updated marker; continuing";
+      }
     }
 
     pthread_mutex_destroy(&params.nti.mu);
@@ -1812,8 +1892,8 @@
     LOG(INFO) << "verified partition contents; update may be resumed";
   }
 
-  if (ota_fsync(params.fd) == -1) {
-    failure_type = kFsyncFailure;
+  if (fsync(params.fd) == -1) {
+    failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
     PLOG(ERROR) << "fsync failed";
   }
   // params.fd will be automatically closed because it's a unique_fd.
@@ -1886,38 +1966,46 @@
  */
 Value* BlockImageVerifyFn(const char* name, State* state,
                           const std::vector<std::unique_ptr<Expr>>& argv) {
-    // Commands which are not tested are set to nullptr to skip them completely
-    const Command commands[] = {
-        { "bsdiff",     PerformCommandDiff  },
-        { "erase",      nullptr             },
-        { "free",       PerformCommandFree  },
-        { "imgdiff",    PerformCommandDiff  },
-        { "move",       PerformCommandMove  },
-        { "new",        nullptr             },
-        { "stash",      PerformCommandStash },
-        { "zero",       nullptr             }
-    };
+  // Commands which are not allowed are set to nullptr to skip them completely.
+  const CommandMap command_map{
+    // clang-format off
+    { Command::Type::ABORT,             PerformCommandAbort },
+    { Command::Type::BSDIFF,            PerformCommandDiff },
+    { Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
+    { Command::Type::ERASE,             nullptr },
+    { Command::Type::FREE,              PerformCommandFree },
+    { Command::Type::IMGDIFF,           PerformCommandDiff },
+    { Command::Type::MOVE,              PerformCommandMove },
+    { Command::Type::NEW,               nullptr },
+    { Command::Type::STASH,             PerformCommandStash },
+    { Command::Type::ZERO,              nullptr },
+    // clang-format on
+  };
+  CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
 
-    // Perform a dry run without writing to test if an update can proceed
-    return PerformBlockImageUpdate(name, state, argv, commands,
-                sizeof(commands) / sizeof(commands[0]), true);
+  // Perform a dry run without writing to test if an update can proceed.
+  return PerformBlockImageUpdate(name, state, argv, command_map, true);
 }
 
 Value* BlockImageUpdateFn(const char* name, State* state,
                           const std::vector<std::unique_ptr<Expr>>& argv) {
-    const Command commands[] = {
-        { "bsdiff",     PerformCommandDiff  },
-        { "erase",      PerformCommandErase },
-        { "free",       PerformCommandFree  },
-        { "imgdiff",    PerformCommandDiff  },
-        { "move",       PerformCommandMove  },
-        { "new",        PerformCommandNew   },
-        { "stash",      PerformCommandStash },
-        { "zero",       PerformCommandZero  }
-    };
+  const CommandMap command_map{
+    // clang-format off
+    { Command::Type::ABORT,             PerformCommandAbort },
+    { Command::Type::BSDIFF,            PerformCommandDiff },
+    { Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
+    { Command::Type::ERASE,             PerformCommandErase },
+    { Command::Type::FREE,              PerformCommandFree },
+    { Command::Type::IMGDIFF,           PerformCommandDiff },
+    { Command::Type::MOVE,              PerformCommandMove },
+    { Command::Type::NEW,               PerformCommandNew },
+    { Command::Type::STASH,             PerformCommandStash },
+    { Command::Type::ZERO,              PerformCommandZero },
+    // clang-format on
+  };
+  CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
 
-    return PerformBlockImageUpdate(name, state, argv, commands,
-                sizeof(commands) / sizeof(commands[0]), false);
+  return PerformBlockImageUpdate(name, state, argv, command_map, false);
 }
 
 Value* RangeSha1Fn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
@@ -1934,18 +2022,19 @@
   const std::unique_ptr<Value>& blockdev_filename = args[0];
   const std::unique_ptr<Value>& ranges = args[1];
 
-  if (blockdev_filename->type != VAL_STRING) {
+  if (blockdev_filename->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name);
     return StringValue("");
   }
-  if (ranges->type != VAL_STRING) {
+  if (ranges->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name);
     return StringValue("");
   }
 
-  android::base::unique_fd fd(ota_open(blockdev_filename->data.c_str(), O_RDWR));
+  android::base::unique_fd fd(open(blockdev_filename->data.c_str(), O_RDWR));
   if (fd == -1) {
-    ErrorAbort(state, kFileOpenFailure, "open \"%s\" failed: %s", blockdev_filename->data.c_str(),
+    CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure;
+    ErrorAbort(state, cause_code, "open \"%s\" failed: %s", blockdev_filename->data.c_str(),
                strerror(errno));
     return StringValue("");
   }
@@ -1957,16 +2046,17 @@
   SHA1_Init(&ctx);
 
   std::vector<uint8_t> buffer(BLOCKSIZE);
-  for (const auto& range : rs) {
-    if (!check_lseek(fd, static_cast<off64_t>(range.first) * BLOCKSIZE, SEEK_SET)) {
+  for (const auto& [begin, end] : rs) {
+    if (!check_lseek(fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
       ErrorAbort(state, kLseekFailure, "failed to seek %s: %s", blockdev_filename->data.c_str(),
                  strerror(errno));
       return StringValue("");
     }
 
-    for (size_t j = range.first; j < range.second; ++j) {
-      if (read_all(fd, buffer, BLOCKSIZE) == -1) {
-        ErrorAbort(state, kFreadFailure, "failed to read %s: %s", blockdev_filename->data.c_str(),
+    for (size_t j = begin; j < end; ++j) {
+      if (!android::base::ReadFully(fd, buffer.data(), BLOCKSIZE)) {
+        CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure;
+        ErrorAbort(state, cause_code, "failed to read %s: %s", blockdev_filename->data.c_str(),
                    strerror(errno));
         return StringValue("");
       }
@@ -2000,14 +2090,15 @@
 
   const std::unique_ptr<Value>& arg_filename = args[0];
 
-  if (arg_filename->type != VAL_STRING) {
+  if (arg_filename->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name);
     return StringValue("");
   }
 
-  android::base::unique_fd fd(ota_open(arg_filename->data.c_str(), O_RDONLY));
+  android::base::unique_fd fd(open(arg_filename->data.c_str(), O_RDONLY));
   if (fd == -1) {
-    ErrorAbort(state, kFileOpenFailure, "open \"%s\" failed: %s", arg_filename->data.c_str(),
+    CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure;
+    ErrorAbort(state, cause_code, "open \"%s\" failed: %s", arg_filename->data.c_str(),
                strerror(errno));
     return StringValue("");
   }
@@ -2015,8 +2106,9 @@
   RangeSet blk0(std::vector<Range>{ Range{ 0, 1 } });
   std::vector<uint8_t> block0_buffer(BLOCKSIZE);
 
-  if (ReadBlocks(blk0, block0_buffer, fd) == -1) {
-    ErrorAbort(state, kFreadFailure, "failed to read %s: %s", arg_filename->data.c_str(),
+  if (ReadBlocks(blk0, &block0_buffer, fd) == -1) {
+    CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure;
+    ErrorAbort(state, cause_code, "failed to read %s: %s", arg_filename->data.c_str(),
                strerror(errno));
     return StringValue("");
   }
@@ -2055,11 +2147,11 @@
   const std::unique_ptr<Value>& filename = args[0];
   const std::unique_ptr<Value>& ranges = args[1];
 
-  if (filename->type != VAL_STRING) {
+  if (filename->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name);
     return StringValue("");
   }
-  if (ranges->type != VAL_STRING) {
+  if (ranges->type != Value::Type::STRING) {
     ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name);
     return StringValue("");
   }
@@ -2093,8 +2185,8 @@
   }
 
   uint8_t buffer[BLOCKSIZE];
-  for (const auto& range : rs) {
-    for (size_t j = range.first; j < range.second; ++j) {
+  for (const auto& [begin, end] : rs) {
+    for (size_t j = begin; j < end; ++j) {
       // Stay within the data area, libfec validates and corrects metadata
       if (status.data_size <= static_cast<uint64_t>(j) * BLOCKSIZE) {
         continue;
diff --git a/updater/commands.cpp b/updater/commands.cpp
new file mode 100644
index 0000000..aed6336
--- /dev/null
+++ b/updater/commands.cpp
@@ -0,0 +1,454 @@
+/*
+ * 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 "private/commands.h"
+
+#include <stdint.h>
+#include <string.h>
+
+#include <functional>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <openssl/sha.h>
+
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
+
+using namespace std::string_literals;
+
+bool Command::abort_allowed_ = false;
+
+Command::Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info)
+    : type_(type),
+      index_(index),
+      cmdline_(std::move(cmdline)),
+      hash_tree_info_(std::move(hash_tree_info)) {
+  CHECK(type == Type::COMPUTE_HASH_TREE);
+}
+
+Command::Type Command::ParseType(const std::string& type_str) {
+  if (type_str == "abort") {
+    if (!abort_allowed_) {
+      LOG(ERROR) << "ABORT disallowed";
+      return Type::LAST;
+    }
+    return Type::ABORT;
+  } else if (type_str == "bsdiff") {
+    return Type::BSDIFF;
+  } else if (type_str == "compute_hash_tree") {
+    return Type::COMPUTE_HASH_TREE;
+  } else if (type_str == "erase") {
+    return Type::ERASE;
+  } else if (type_str == "free") {
+    return Type::FREE;
+  } else if (type_str == "imgdiff") {
+    return Type::IMGDIFF;
+  } else if (type_str == "move") {
+    return Type::MOVE;
+  } else if (type_str == "new") {
+    return Type::NEW;
+  } else if (type_str == "stash") {
+    return Type::STASH;
+  } else if (type_str == "zero") {
+    return Type::ZERO;
+  }
+  return Type::LAST;
+};
+
+bool Command::ParseTargetInfoAndSourceInfo(const std::vector<std::string>& tokens,
+                                           const std::string& tgt_hash, TargetInfo* target,
+                                           const std::string& src_hash, SourceInfo* source,
+                                           std::string* err) {
+  // We expect the given args (in 'tokens' vector) in one of the following formats.
+  //
+  //    <tgt_ranges> <src_block_count> - <[stash_id:location] ...>
+  //        (loads data from stashes only)
+  //
+  //    <tgt_ranges> <src_block_count> <src_ranges>
+  //        (loads data from source image only)
+  //
+  //    <tgt_ranges> <src_block_count> <src_ranges> <src_ranges_location> <[stash_id:location] ...>
+  //        (loads data from both of source image and stashes)
+
+  // At least it needs to provide three args: <tgt_ranges>, <src_block_count> and "-"/<src_ranges>.
+  if (tokens.size() < 3) {
+    *err = "invalid number of args";
+    return false;
+  }
+
+  size_t pos = 0;
+  RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]);
+  if (!tgt_ranges) {
+    *err = "invalid target ranges";
+    return false;
+  }
+  *target = TargetInfo(tgt_hash, tgt_ranges);
+
+  // <src_block_count>
+  const std::string& token = tokens[pos++];
+  size_t src_blocks;
+  if (!android::base::ParseUint(token, &src_blocks)) {
+    *err = "invalid src_block_count \""s + token + "\"";
+    return false;
+  }
+
+  RangeSet src_ranges;
+  RangeSet src_ranges_location;
+  // "-" or <src_ranges> [<src_ranges_location>]
+  if (tokens[pos] == "-") {
+    // no source ranges, only stashes
+    pos++;
+  } else {
+    src_ranges = RangeSet::Parse(tokens[pos++]);
+    if (!src_ranges) {
+      *err = "invalid source ranges";
+      return false;
+    }
+
+    if (pos >= tokens.size()) {
+      // No stashes, only source ranges.
+      SourceInfo result(src_hash, src_ranges, {}, {});
+
+      // Sanity check the block count.
+      if (result.blocks() != src_blocks) {
+        *err =
+            android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(),
+                                        src_ranges.ToString().c_str(), src_blocks);
+        return false;
+      }
+
+      *source = result;
+      return true;
+    }
+
+    src_ranges_location = RangeSet::Parse(tokens[pos++]);
+    if (!src_ranges_location) {
+      *err = "invalid source ranges location";
+      return false;
+    }
+  }
+
+  // <[stash_id:stash_location]>
+  std::vector<StashInfo> stashes;
+  while (pos < tokens.size()) {
+    // Each word is a an index into the stash table, a colon, and then a RangeSet describing where
+    // in the source block that stashed data should go.
+    std::vector<std::string> pairs = android::base::Split(tokens[pos++], ":");
+    if (pairs.size() != 2) {
+      *err = "invalid stash info";
+      return false;
+    }
+    RangeSet stash_location = RangeSet::Parse(pairs[1]);
+    if (!stash_location) {
+      *err = "invalid stash location";
+      return false;
+    }
+    stashes.emplace_back(pairs[0], stash_location);
+  }
+
+  SourceInfo result(src_hash, src_ranges, src_ranges_location, stashes);
+  if (src_blocks != result.blocks()) {
+    *err = android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(),
+                                       src_ranges.ToString().c_str(), src_blocks);
+    return false;
+  }
+
+  *source = result;
+  return true;
+}
+
+Command Command::Parse(const std::string& line, size_t index, std::string* err) {
+  std::vector<std::string> tokens = android::base::Split(line, " ");
+  size_t pos = 0;
+  // tokens.size() will be 1 at least.
+  Type op = ParseType(tokens[pos++]);
+  if (op == Type::LAST) {
+    *err = "invalid type";
+    return {};
+  }
+
+  PatchInfo patch_info;
+  TargetInfo target_info;
+  SourceInfo source_info;
+  StashInfo stash_info;
+
+  if (op == Type::ZERO || op == Type::NEW || op == Type::ERASE) {
+    // zero/new/erase <rangeset>
+    if (pos + 1 != tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)",
+                                         tokens.size() - pos);
+      return {};
+    }
+    RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]);
+    if (!tgt_ranges) {
+      return {};
+    }
+    static const std::string kUnknownHash{ "unknown-hash" };
+    target_info = TargetInfo(kUnknownHash, tgt_ranges);
+  } else if (op == Type::STASH) {
+    // stash <stash_id> <src_ranges>
+    if (pos + 2 != tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 2)",
+                                         tokens.size() - pos);
+      return {};
+    }
+    const std::string& id = tokens[pos++];
+    RangeSet src_ranges = RangeSet::Parse(tokens[pos++]);
+    if (!src_ranges) {
+      *err = "invalid token";
+      return {};
+    }
+    stash_info = StashInfo(id, src_ranges);
+  } else if (op == Type::FREE) {
+    // free <stash_id>
+    if (pos + 1 != tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)",
+                                         tokens.size() - pos);
+      return {};
+    }
+    stash_info = StashInfo(tokens[pos++], {});
+  } else if (op == Type::MOVE) {
+    // <hash>
+    if (pos + 1 > tokens.size()) {
+      *err = "missing hash";
+      return {};
+    }
+    std::string hash = tokens[pos++];
+    if (!ParseTargetInfoAndSourceInfo(
+            std::vector<std::string>(tokens.cbegin() + pos, tokens.cend()), hash, &target_info,
+            hash, &source_info, err)) {
+      return {};
+    }
+  } else if (op == Type::BSDIFF || op == Type::IMGDIFF) {
+    // <offset> <length> <srchash> <dsthash>
+    if (pos + 4 > tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 4+)",
+                                         tokens.size() - pos);
+      return {};
+    }
+    size_t offset;
+    size_t length;
+    if (!android::base::ParseUint(tokens[pos++], &offset) ||
+        !android::base::ParseUint(tokens[pos++], &length)) {
+      *err = "invalid patch offset/length";
+      return {};
+    }
+    patch_info = PatchInfo(offset, length);
+
+    std::string src_hash = tokens[pos++];
+    std::string dst_hash = tokens[pos++];
+    if (!ParseTargetInfoAndSourceInfo(
+            std::vector<std::string>(tokens.cbegin() + pos, tokens.cend()), dst_hash, &target_info,
+            src_hash, &source_info, err)) {
+      return {};
+    }
+  } else if (op == Type::ABORT) {
+    // No-op, other than sanity checking the input args.
+    if (pos != tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 0)",
+                                         tokens.size() - pos);
+      return {};
+    }
+  } else if (op == Type::COMPUTE_HASH_TREE) {
+    // <hash_tree_ranges> <source_ranges> <hash_algorithm> <salt_hex> <root_hash>
+    if (pos + 5 != tokens.size()) {
+      *err = android::base::StringPrintf("invalid number of args: %zu (expected 5)",
+                                         tokens.size() - pos);
+      return {};
+    }
+
+    // Expects the hash_tree data to be contiguous.
+    RangeSet hash_tree_ranges = RangeSet::Parse(tokens[pos++]);
+    if (!hash_tree_ranges || hash_tree_ranges.size() != 1) {
+      *err = "invalid hash tree ranges in: " + line;
+      return {};
+    }
+
+    RangeSet source_ranges = RangeSet::Parse(tokens[pos++]);
+    if (!source_ranges) {
+      *err = "invalid source ranges in: " + line;
+      return {};
+    }
+
+    std::string hash_algorithm = tokens[pos++];
+    std::string salt_hex = tokens[pos++];
+    std::string root_hash = tokens[pos++];
+    if (hash_algorithm.empty() || salt_hex.empty() || root_hash.empty()) {
+      *err = "invalid hash tree arguments in " + line;
+      return {};
+    }
+
+    HashTreeInfo hash_tree_info(std::move(hash_tree_ranges), std::move(source_ranges),
+                                std::move(hash_algorithm), std::move(salt_hex),
+                                std::move(root_hash));
+    return Command(op, index, line, std::move(hash_tree_info));
+  } else {
+    *err = "invalid op";
+    return {};
+  }
+
+  return Command(op, index, line, patch_info, target_info, source_info, stash_info);
+}
+
+bool SourceInfo::Overlaps(const TargetInfo& target) const {
+  return ranges_.Overlaps(target.ranges());
+}
+
+// Moves blocks in the 'source' vector to the specified locations (as in 'locs') in the 'dest'
+// vector. Note that source and dest may be the same buffer.
+static void MoveRange(std::vector<uint8_t>* dest, const RangeSet& locs,
+                      const std::vector<uint8_t>& source, size_t block_size) {
+  const uint8_t* from = source.data();
+  uint8_t* to = dest->data();
+  size_t start = locs.blocks();
+  // Must do the movement backward.
+  for (auto it = locs.crbegin(); it != locs.crend(); it++) {
+    size_t blocks = it->second - it->first;
+    start -= blocks;
+    memmove(to + (it->first * block_size), from + (start * block_size), blocks * block_size);
+  }
+}
+
+bool SourceInfo::ReadAll(
+    std::vector<uint8_t>* buffer, size_t block_size,
+    const std::function<int(const RangeSet&, std::vector<uint8_t>*)>& block_reader,
+    const std::function<int(const std::string&, std::vector<uint8_t>*)>& stash_reader) const {
+  if (buffer->size() < blocks() * block_size) {
+    return false;
+  }
+
+  // Read in the source ranges.
+  if (ranges_) {
+    if (block_reader(ranges_, buffer) != 0) {
+      return false;
+    }
+    if (location_) {
+      MoveRange(buffer, location_, *buffer, block_size);
+    }
+  }
+
+  // Read in the stashes.
+  for (const StashInfo& stash : stashes_) {
+    std::vector<uint8_t> stash_buffer(stash.blocks() * block_size);
+    if (stash_reader(stash.id(), &stash_buffer) != 0) {
+      return false;
+    }
+    MoveRange(buffer, stash.ranges(), stash_buffer, block_size);
+  }
+  return true;
+}
+
+void SourceInfo::DumpBuffer(const std::vector<uint8_t>& buffer, size_t block_size) const {
+  LOG(INFO) << "Dumping hashes in hex for " << ranges_.blocks() << " source blocks";
+
+  const RangeSet& location = location_ ? location_ : RangeSet({ Range{ 0, ranges_.blocks() } });
+  for (size_t i = 0; i < ranges_.blocks(); i++) {
+    size_t block_num = ranges_.GetBlockNumber(i);
+    size_t buffer_index = location.GetBlockNumber(i);
+    CHECK_LE((buffer_index + 1) * block_size, buffer.size());
+
+    uint8_t digest[SHA_DIGEST_LENGTH];
+    SHA1(buffer.data() + buffer_index * block_size, block_size, digest);
+    std::string hexdigest = print_sha1(digest);
+    LOG(INFO) << "  block number: " << block_num << ", SHA-1: " << hexdigest;
+  }
+}
+
+std::ostream& operator<<(std::ostream& os, const Command& command) {
+  os << command.index() << ": " << command.cmdline();
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const TargetInfo& target) {
+  os << target.blocks() << " blocks (" << target.hash_ << "): " << target.ranges_.ToString();
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const StashInfo& stash) {
+  os << stash.blocks() << " blocks (" << stash.id_ << "): " << stash.ranges_.ToString();
+  return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const SourceInfo& source) {
+  os << source.blocks_ << " blocks (" << source.hash_ << "): ";
+  if (source.ranges_) {
+    os << source.ranges_.ToString();
+    if (source.location_) {
+      os << " (location: " << source.location_.ToString() << ")";
+    }
+  }
+  if (!source.stashes_.empty()) {
+    os << " " << source.stashes_.size() << " stash(es)";
+  }
+  return os;
+}
+
+TransferList TransferList::Parse(const std::string& transfer_list_str, std::string* err) {
+  TransferList result{};
+
+  std::vector<std::string> lines = android::base::Split(transfer_list_str, "\n");
+  if (lines.size() < kTransferListHeaderLines) {
+    *err = android::base::StringPrintf("too few lines in the transfer list [%zu]", lines.size());
+    return TransferList{};
+  }
+
+  // First line in transfer list is the version number.
+  if (!android::base::ParseInt(lines[0], &result.version_, 3, 4)) {
+    *err = "unexpected transfer list version ["s + lines[0] + "]";
+    return TransferList{};
+  }
+
+  // Second line in transfer list is the total number of blocks we expect to write.
+  if (!android::base::ParseUint(lines[1], &result.total_blocks_)) {
+    *err = "unexpected block count ["s + lines[1] + "]";
+    return TransferList{};
+  }
+
+  // Third line is how many stash entries are needed simultaneously.
+  if (!android::base::ParseUint(lines[2], &result.stash_max_entries_)) {
+    return TransferList{};
+  }
+
+  // Fourth line is the maximum number of blocks that will be stashed simultaneously.
+  if (!android::base::ParseUint(lines[3], &result.stash_max_blocks_)) {
+    *err = "unexpected maximum stash blocks ["s + lines[3] + "]";
+    return TransferList{};
+  }
+
+  // Subsequent lines are all individual transfer commands.
+  for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) {
+    const std::string& line = lines[i];
+    if (line.empty()) continue;
+
+    size_t cmdindex = i - kTransferListHeaderLines;
+    std::string parsing_error;
+    Command command = Command::Parse(line, cmdindex, &parsing_error);
+    if (!command) {
+      *err = android::base::StringPrintf("Failed to parse command %zu [%s]: %s", cmdindex,
+                                         line.c_str(), parsing_error.c_str());
+      return TransferList{};
+    }
+    result.commands_.push_back(command);
+  }
+
+  return result;
+}
diff --git a/updater/include/private/commands.h b/updater/include/private/commands.h
new file mode 100644
index 0000000..79f9154
--- /dev/null
+++ b/updater/include/private/commands.h
@@ -0,0 +1,475 @@
+/*
+ * 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.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <functional>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest_prod.h>  // FRIEND_TEST
+
+#include "otautil/rangeset.h"
+
+// Represents the target info used in a Command. TargetInfo contains the ranges of the blocks and
+// the expected hash.
+class TargetInfo {
+ public:
+  TargetInfo() = default;
+
+  TargetInfo(std::string hash, RangeSet ranges)
+      : hash_(std::move(hash)), ranges_(std::move(ranges)) {}
+
+  const std::string& hash() const {
+    return hash_;
+  }
+
+  const RangeSet& ranges() const {
+    return ranges_;
+  }
+
+  size_t blocks() const {
+    return ranges_.blocks();
+  }
+
+  bool operator==(const TargetInfo& other) const {
+    return hash_ == other.hash_ && ranges_ == other.ranges_;
+  }
+
+ private:
+  friend std::ostream& operator<<(std::ostream& os, const TargetInfo& source);
+
+  // The hash of the data represented by the object.
+  std::string hash_;
+  // The block ranges that the data should be written to.
+  RangeSet ranges_;
+};
+
+std::ostream& operator<<(std::ostream& os, const TargetInfo& source);
+
+// Represents the stash info used in a Command.
+class StashInfo {
+ public:
+  StashInfo() = default;
+
+  StashInfo(std::string id, RangeSet ranges) : id_(std::move(id)), ranges_(std::move(ranges)) {}
+
+  size_t blocks() const {
+    return ranges_.blocks();
+  }
+
+  const std::string& id() const {
+    return id_;
+  }
+
+  const RangeSet& ranges() const {
+    return ranges_;
+  }
+
+  bool operator==(const StashInfo& other) const {
+    return id_ == other.id_ && ranges_ == other.ranges_;
+  }
+
+ private:
+  friend std::ostream& operator<<(std::ostream& os, const StashInfo& stash);
+
+  // The id (i.e. hash) of the stash.
+  std::string id_;
+  // The matching location of the stash.
+  RangeSet ranges_;
+};
+
+std::ostream& operator<<(std::ostream& os, const StashInfo& stash);
+
+// Represents the source info in a Command, whose data could come from source image, stashed blocks,
+// or both.
+class SourceInfo {
+ public:
+  SourceInfo() = default;
+
+  SourceInfo(std::string hash, RangeSet ranges, RangeSet location, std::vector<StashInfo> stashes)
+      : hash_(std::move(hash)),
+        ranges_(std::move(ranges)),
+        location_(std::move(location)),
+        stashes_(std::move(stashes)) {
+    blocks_ = ranges_.blocks();
+    for (const auto& stash : stashes_) {
+      blocks_ += stash.ranges().blocks();
+    }
+  }
+
+  // Reads all the data specified by this SourceInfo object into the given 'buffer', by calling the
+  // given readers. Caller needs to specify the block size for the represented blocks. The given
+  // buffer needs to be sufficiently large. Otherwise it returns false. 'block_reader' and
+  // 'stash_reader' read the specified data into the given buffer (guaranteed to be large enough)
+  // respectively. The readers should return 0 on success, or -1 on error.
+  bool ReadAll(
+      std::vector<uint8_t>* buffer, size_t block_size,
+      const std::function<int(const RangeSet&, std::vector<uint8_t>*)>& block_reader,
+      const std::function<int(const std::string&, std::vector<uint8_t>*)>& stash_reader) const;
+
+  // Whether this SourceInfo overlaps with the given TargetInfo object.
+  bool Overlaps(const TargetInfo& target) const;
+
+  // Dumps the hashes in hex for the given buffer that's loaded from this SourceInfo object
+  // (excluding the stashed blocks which are handled separately).
+  void DumpBuffer(const std::vector<uint8_t>& buffer, size_t block_size) const;
+
+  const std::string& hash() const {
+    return hash_;
+  }
+
+  size_t blocks() const {
+    return blocks_;
+  }
+
+  bool operator==(const SourceInfo& other) const {
+    return hash_ == other.hash_ && ranges_ == other.ranges_ && location_ == other.location_ &&
+           stashes_ == other.stashes_;
+  }
+
+ private:
+  friend std::ostream& operator<<(std::ostream& os, const SourceInfo& source);
+
+  // The hash of the data represented by the object.
+  std::string hash_;
+  // The block ranges from the source image to read data from. This could be a subset of all the
+  // blocks represented by the object, or empty if all the data should be loaded from stash.
+  RangeSet ranges_;
+  // The location in the buffer to load ranges_ into. Empty if ranges_ alone covers all the blocks
+  // (i.e. nothing needs to be loaded from stash).
+  RangeSet location_;
+  // The info for the stashed blocks that are part of the source. Empty if there's none.
+  std::vector<StashInfo> stashes_;
+  // Total number of blocks represented by the object.
+  size_t blocks_{ 0 };
+};
+
+std::ostream& operator<<(std::ostream& os, const SourceInfo& source);
+
+class PatchInfo {
+ public:
+  PatchInfo() = default;
+
+  PatchInfo(size_t offset, size_t length) : offset_(offset), length_(length) {}
+
+  size_t offset() const {
+    return offset_;
+  }
+
+  size_t length() const {
+    return length_;
+  }
+
+  bool operator==(const PatchInfo& other) const {
+    return offset_ == other.offset_ && length_ == other.length_;
+  }
+
+ private:
+  size_t offset_{ 0 };
+  size_t length_{ 0 };
+};
+
+// The arguments to build a hash tree from blocks on the block device.
+class HashTreeInfo {
+ public:
+  HashTreeInfo() = default;
+
+  HashTreeInfo(RangeSet hash_tree_ranges, RangeSet source_ranges, std::string hash_algorithm,
+               std::string salt_hex, std::string root_hash)
+      : hash_tree_ranges_(std::move(hash_tree_ranges)),
+        source_ranges_(std::move(source_ranges)),
+        hash_algorithm_(std::move(hash_algorithm)),
+        salt_hex_(std::move(salt_hex)),
+        root_hash_(std::move(root_hash)) {}
+
+  const RangeSet& hash_tree_ranges() const {
+    return hash_tree_ranges_;
+  }
+  const RangeSet& source_ranges() const {
+    return source_ranges_;
+  }
+
+  const std::string& hash_algorithm() const {
+    return hash_algorithm_;
+  }
+  const std::string& salt_hex() const {
+    return salt_hex_;
+  }
+  const std::string& root_hash() const {
+    return root_hash_;
+  }
+
+  bool operator==(const HashTreeInfo& other) const {
+    return hash_tree_ranges_ == other.hash_tree_ranges_ && source_ranges_ == other.source_ranges_ &&
+           hash_algorithm_ == other.hash_algorithm_ && salt_hex_ == other.salt_hex_ &&
+           root_hash_ == other.root_hash_;
+  }
+
+ private:
+  RangeSet hash_tree_ranges_;
+  RangeSet source_ranges_;
+  std::string hash_algorithm_;
+  std::string salt_hex_;
+  std::string root_hash_;
+};
+
+// Command class holds the info for an update command that performs block-based OTA (BBOTA). Each
+// command consists of one or several args, namely TargetInfo, SourceInfo, StashInfo and PatchInfo.
+// The currently used BBOTA version is v4.
+//
+//    zero <tgt_ranges>
+//      - Fill the indicated blocks with zeros.
+//      - Meaningful args: TargetInfo
+//
+//    new <tgt_ranges>
+//      - Fill the blocks with data read from the new_data file.
+//      - Meaningful args: TargetInfo
+//
+//    erase <tgt_ranges>
+//      - Mark the given blocks as empty.
+//      - Meaningful args: TargetInfo
+//
+//    move <hash> <...>
+//      - Read the source blocks, write result to target blocks.
+//      - Meaningful args: TargetInfo, SourceInfo
+//
+//      See the note below for <...>.
+//
+//    bsdiff <patchstart> <patchlen> <srchash> <dsthash> <...>
+//    imgdiff <patchstart> <patchlen> <srchash> <dsthash> <...>
+//      - Read the source blocks, apply a patch, and write result to target blocks.
+//      - Meaningful args: PatchInfo, TargetInfo, SourceInfo
+//
+//      It expects <...> in one of the following formats:
+//
+//        <tgt_ranges> <src_block_count> - <[stash_id:stash_location] ...>
+//          (loads data from stashes only)
+//
+//        <tgt_ranges> <src_block_count> <src_ranges>
+//          (loads data from source image only)
+//
+//        <tgt_ranges> <src_block_count> <src_ranges> <src_ranges_location>
+//                                       <[stash_id:stash_location] ...>
+//          (loads data from both of source image and stashes)
+//
+//    stash <stash_id> <src_ranges>
+//      - Load the given source blocks and stash the data in the given slot of the stash table.
+//      - Meaningful args: StashInfo
+//
+//    free <stash_id>
+//      - Free the given stash data.
+//      - Meaningful args: StashInfo
+//
+//    compute_hash_tree <hash_tree_ranges> <source_ranges> <hash_algorithm> <salt_hex> <root_hash>
+//      - Computes the hash_tree bytes and writes the result to the specified range on the
+//        block_device.
+//
+//    abort
+//      - Abort the current update. Allowed for testing code only.
+//
+class Command {
+ public:
+  enum class Type {
+    ABORT,
+    BSDIFF,
+    COMPUTE_HASH_TREE,
+    ERASE,
+    FREE,
+    IMGDIFF,
+    MOVE,
+    NEW,
+    STASH,
+    ZERO,
+    LAST,  // Not a valid type.
+  };
+
+  Command() = default;
+
+  Command(Type type, size_t index, std::string cmdline, PatchInfo patch, TargetInfo target,
+          SourceInfo source, StashInfo stash)
+      : type_(type),
+        index_(index),
+        cmdline_(std::move(cmdline)),
+        patch_(std::move(patch)),
+        target_(std::move(target)),
+        source_(std::move(source)),
+        stash_(std::move(stash)) {}
+
+  Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info);
+
+  // Parses the given command 'line' into a Command object and returns it. The 'index' is specified
+  // by the caller to index the object. On parsing error, it returns an empty Command object that
+  // evaluates to false, and the specific error message will be set in 'err'.
+  static Command Parse(const std::string& line, size_t index, std::string* err);
+
+  // Parses the command type from the given string.
+  static Type ParseType(const std::string& type_str);
+
+  Type type() const {
+    return type_;
+  }
+
+  size_t index() const {
+    return index_;
+  }
+
+  const std::string& cmdline() const {
+    return cmdline_;
+  }
+
+  const PatchInfo& patch() const {
+    return patch_;
+  }
+
+  const TargetInfo& target() const {
+    return target_;
+  }
+
+  const SourceInfo& source() const {
+    return source_;
+  }
+
+  const StashInfo& stash() const {
+    return stash_;
+  }
+
+  const HashTreeInfo& hash_tree_info() const {
+    return hash_tree_info_;
+  }
+
+  size_t block_size() const {
+    return block_size_;
+  }
+
+  constexpr explicit operator bool() const {
+    return type_ != Type::LAST;
+  }
+
+ private:
+  friend class ResumableUpdaterTest;
+  friend class UpdaterTest;
+
+  FRIEND_TEST(CommandsTest, Parse_ABORT_Allowed);
+  FRIEND_TEST(CommandsTest, Parse_InvalidNumberOfArgs);
+  FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput);
+  FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly);
+  FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes);
+  FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly);
+
+  // Parses the target and source info from the given 'tokens' vector. Saves the parsed info into
+  // 'target' and 'source' objects. Returns the parsing result. Error message will be set in 'err'
+  // on parsing error, and the contents in 'target' and 'source' will be undefined.
+  static bool ParseTargetInfoAndSourceInfo(const std::vector<std::string>& tokens,
+                                           const std::string& tgt_hash, TargetInfo* target,
+                                           const std::string& src_hash, SourceInfo* source,
+                                           std::string* err);
+
+  // Allows parsing ABORT command, which should be used for testing purpose only.
+  static bool abort_allowed_;
+
+  // The type of the command.
+  Type type_{ Type::LAST };
+  // The index of the Command object, which is specified by the caller.
+  size_t index_{ 0 };
+  // The input string that the Command object is parsed from.
+  std::string cmdline_;
+  // The patch info. Only meaningful for BSDIFF and IMGDIFF commands.
+  PatchInfo patch_;
+  // The target info, where the command should be written to.
+  TargetInfo target_;
+  // The source info to load the source blocks for the command.
+  SourceInfo source_;
+  // The stash info. Only meaningful for STASH and FREE commands. Note that although SourceInfo may
+  // also load data from stash, such info will be owned and managed by SourceInfo (i.e. in source_).
+  StashInfo stash_;
+  // The hash_tree info. Only meaningful for COMPUTE_HASH_TREE.
+  HashTreeInfo hash_tree_info_;
+  // The unit size of each block to be used in this command.
+  size_t block_size_{ 4096 };
+};
+
+std::ostream& operator<<(std::ostream& os, const Command& command);
+
+// TransferList represents the info for a transfer list, which is parsed from input text lines
+// containing commands to transfer data from one place to another on the target partition.
+//
+// The creator of the transfer list will guarantee that no block is read (i.e., used as the source
+// for a patch or move) after it has been written.
+//
+// The creator will guarantee that a given stash is loaded (with a stash command) before it's used
+// in a move/bsdiff/imgdiff command.
+//
+// Within one command the source and target ranges may overlap so in general we need to read the
+// entire source into memory before writing anything to the target blocks.
+//
+// All the patch data is concatenated into one patch_data file in the update package. It must be
+// stored uncompressed because we memory-map it in directly from the archive. (Since patches are
+// already compressed, we lose very little by not compressing their concatenation.)
+//
+// Commands that read data from the partition (i.e. move/bsdiff/imgdiff/stash) have one or more
+// additional hashes before the range parameters, which are used to check if the command has
+// already been completed and verify the integrity of the source data.
+class TransferList {
+ public:
+  // Number of header lines.
+  static constexpr size_t kTransferListHeaderLines = 4;
+
+  TransferList() = default;
+
+  // Parses the given input string and returns a TransferList object. Sets error message if any.
+  static TransferList Parse(const std::string& transfer_list_str, std::string* err);
+
+  int version() const {
+    return version_;
+  }
+
+  size_t total_blocks() const {
+    return total_blocks_;
+  }
+
+  size_t stash_max_entries() const {
+    return stash_max_entries_;
+  }
+
+  size_t stash_max_blocks() const {
+    return stash_max_blocks_;
+  }
+
+  const std::vector<Command>& commands() const {
+    return commands_;
+  }
+
+  // Returns whether the TransferList is valid.
+  constexpr explicit operator bool() const {
+    return version_ != 0;
+  }
+
+ private:
+  // BBOTA version.
+  int version_{ 0 };
+  // Total number of blocks to be written in this transfer.
+  size_t total_blocks_;
+  // Maximum number of stashes that exist at the same time.
+  size_t stash_max_entries_;
+  // Maximum number of blocks to be stashed.
+  size_t stash_max_blocks_;
+  // Commands in this transfer.
+  std::vector<Command> commands_;
+};
diff --git a/updater/install.cpp b/updater/install.cpp
index 9be7645..deb7a2b 100644
--- a/updater/install.cpp
+++ b/updater/install.cpp
@@ -46,9 +46,9 @@
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
+#include <android-base/unique_fd.h>
 #include <applypatch/applypatch.h>
 #include <bootloader_message/bootloader_message.h>
-#include <cutils/android_reboot.h>
 #include <ext4_utils/wipe.h>
 #include <openssl/sha.h>
 #include <selinux/label.h>
@@ -57,11 +57,11 @@
 #include <ziparchive/zip_archive.h>
 
 #include "edify/expr.h"
-#include "mounts.h"
-#include "otafault/ota_io.h"
-#include "otautil/DirUtil.h"
+#include "otautil/dirutil.h"
 #include "otautil/error_code.h"
+#include "otautil/mounts.h"
 #include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
 #include "updater/updater.h"
 
 // Send over the buffer to recovery though the command pipe.
@@ -137,8 +137,8 @@
       return StringValue("");
     }
 
-    unique_fd fd(TEMP_FAILURE_RETRY(
-        ota_open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)));
+    android::base::unique_fd fd(TEMP_FAILURE_RETRY(
+        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("");
@@ -152,11 +152,12 @@
                  << "\": " << ErrorCodeString(ret);
       success = false;
     }
-    if (ota_fsync(fd) == -1) {
+    if (fsync(fd) == -1) {
       PLOG(ERROR) << "fsync of \"" << dest_path << "\" failed";
       success = false;
     }
-    if (ota_close(fd) == -1) {
+
+    if (close(fd.release()) != 0) {
       PLOG(ERROR) << "close of \"" << dest_path << "\" failed";
       success = false;
     }
@@ -191,145 +192,86 @@
                         zip_path.c_str(), buffer.size(), ErrorCodeString(ret));
     }
 
-    return new Value(VAL_BLOB, buffer);
+    return new Value(Value::Type::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.
+// patch_partition_check(target_partition, source_partition)
+//   Checks if the target and source partitions have the desired checksums to be patched. It returns
+//   directly, if the target partition already has the expected checksum. Otherwise it in turn
+//   checks the integrity of the source partition and the backup file on /cache.
 //
-//   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) {
+// For example, patch_partition_check(
+//     "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d",
+//     "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4")
+Value* PatchPartitionCheckFn(const char* name, State* state,
+                             const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() != 2) {
     return ErrorAbort(state, kArgsParsingFailure,
-                      "%s(): expected at least 6 args and an "
-                      "even number, got %zu",
-                      name, argv.size());
+                      "%s(): Invalid number of args (expected 2, 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());
+  if (!ReadArgs(state, argv, &args, 0, 2)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
   }
 
-  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;
+  std::string err;
+  auto target = Partition::Parse(args[0], &err);
+  if (!target) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name,
+                      args[0].c_str(), err.c_str());
   }
 
-  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);
-    }
+  auto source = Partition::Parse(args[1], &err);
+  if (!source) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name,
+                      args[1].c_str(), err.c_str());
   }
 
-  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" : "");
+  bool result = PatchPartitionCheck(target, source);
+  return StringValue(result ? "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());
+// patch_partition(target, source, patch)
+//   Applies the given patch to the source partition, and writes the result to the target partition.
+//
+// For example, patch_partition(
+//     "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d",
+//     "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4",
+//     package_extract_file("boot.img.p"))
+Value* PatchPartitionFn(const char* name, State* state,
+                        const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() != 3) {
+    return ErrorAbort(state, kArgsParsingFailure,
+                      "%s(): Invalid number of args (expected 3, 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);
+  if (!ReadArgs(state, argv, &args, 0, 2)) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
   }
 
-  std::vector<std::unique_ptr<Value>> args;
-  if (!ReadValueArgs(state, argv, &args)) {
-    return nullptr;
+  std::string err;
+  auto target = Partition::Parse(args[0], &err);
+  if (!target) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name,
+                      args[0].c_str(), err.c_str());
   }
 
-  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));
+  auto source = Partition::Parse(args[1], &err);
+  if (!source) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name,
+                      args[1].c_str(), err.c_str());
   }
 
-  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();
-    }
+  std::vector<std::unique_ptr<Value>> values;
+  if (!ReadValueArgs(state, argv, &values, 2, 1) || values[0]->type != Value::Type::BLOB) {
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Invalid patch arg", name);
   }
 
-  // Didn't match any of the hex strings; return false.
-  return StringValue("");
+  bool result = PatchPartition(target, source, *values[0], nullptr);
+  return StringValue(result ? "t" : "");
 }
 
 // mount(fs_type, partition_type, location, mount_point)
@@ -511,8 +453,8 @@
   }
 
   if (fs_type == "ext4") {
-    const char* mke2fs_argv[] = { "/sbin/mke2fs_static", "-t",    "ext4", "-b", "4096",
-                                  location.c_str(),      nullptr, nullptr };
+    const char* mke2fs_argv[] = { "/system/bin/mke2fs", "-t",    "ext4", "-b", "4096",
+                                  location.c_str(),     nullptr, nullptr };
     std::string size_str;
     if (size != 0) {
       size_str = std::to_string(size / 4096LL);
@@ -525,8 +467,8 @@
       return StringValue("");
     }
 
-    const char* e2fsdroid_argv[] = { "/sbin/e2fsdroid_static", "-e",   "-a", mount_point.c_str(),
-                                     location.c_str(),         nullptr };
+    const char* e2fsdroid_argv[] = { "/system/bin/e2fsdroid", "-e",   "-a", mount_point.c_str(),
+                                     location.c_str(),        nullptr };
     status = exec_cmd(e2fsdroid_argv[0], const_cast<char**>(e2fsdroid_argv));
     if (status != 0) {
       LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location;
@@ -661,33 +603,12 @@
   const std::string& filename = args[0];
   const std::string& key = args[1];
 
-  struct stat st;
-  if (stat(filename.c_str(), &st) < 0) {
-    return ErrorAbort(state, kFileGetPropFailure, "%s: failed to stat \"%s\": %s", name,
-                      filename.c_str(), strerror(errno));
-  }
-
-  constexpr off_t MAX_FILE_GETPROP_SIZE = 65536;
-  if (st.st_size > MAX_FILE_GETPROP_SIZE) {
-    return ErrorAbort(state, kFileGetPropFailure, "%s too large for %s (max %lld)",
-                      filename.c_str(), name, static_cast<long long>(MAX_FILE_GETPROP_SIZE));
-  }
-
-  std::string buffer(st.st_size, '\0');
-  unique_file f(ota_fopen(filename.c_str(), "rb"));
-  if (f == nullptr) {
-    return ErrorAbort(state, kFileOpenFailure, "%s: failed to open %s: %s", name, filename.c_str(),
-                      strerror(errno));
-  }
-
-  if (ota_fread(&buffer[0], 1, st.st_size, f.get()) != static_cast<size_t>(st.st_size)) {
-    ErrorAbort(state, kFreadFailure, "%s: failed to read %zu bytes from %s", name,
-               static_cast<size_t>(st.st_size), filename.c_str());
+  std::string buffer;
+  if (!android::base::ReadFileToString(filename, &buffer)) {
+    ErrorAbort(state, kFreadFailure, "%s: failed to read %s", name, filename.c_str());
     return nullptr;
   }
 
-  ota_fclose(f);
-
   std::vector<std::string> lines = android::base::Split(buffer, "\n");
   for (size_t i = 0; i < lines.size(); i++) {
     std::string line = android::base::Trim(lines[i]);
@@ -733,7 +654,7 @@
   }
 
   // Skip the cache size check if the update is a retry.
-  if (state->is_retry || CacheSizeCheck(bytes) == 0) {
+  if (state->is_retry || CheckAndFreeSpaceOnCache(bytes)) {
     return StringValue("t");
   }
   return StringValue("");
@@ -786,8 +707,8 @@
   return StringValue(std::to_string(status));
 }
 
-// Read a local file and return its contents (the Value* returned
-// is actually a FileContents*).
+// read_file(filename)
+//   Reads a local file 'filename' and returns its contents as a string Value.
 Value* ReadFileFn(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());
@@ -795,18 +716,18 @@
 
   std::vector<std::string> args;
   if (!ReadArgs(state, argv, &args)) {
-    return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
+    return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
   }
   const std::string& filename = args[0];
 
-  Value* v = new Value(VAL_INVALID, "");
-
-  FileContents fc;
-  if (LoadFileContents(filename.c_str(), &fc) == 0) {
-    v->type = VAL_BLOB;
-    v->data = std::string(fc.data.begin(), fc.data.end());
+  std::string contents;
+  if (android::base::ReadFileToString(filename, &contents)) {
+    return new Value(Value::Type::STRING, std::move(contents));
   }
-  return v;
+
+  // Leave it to caller to handle the failure.
+  PLOG(ERROR) << name << ": Failed to read " << filename;
+  return StringValue("");
 }
 
 // write_value(value, filename)
@@ -872,11 +793,7 @@
     return StringValue("");
   }
 
-  std::string reboot_cmd = "reboot," + property;
-  if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
-    reboot_cmd += ",quiescent";
-  }
-  android::base::SetProperty(ANDROID_RB_PROPERTY, reboot_cmd);
+  reboot("reboot," + property);
 
   sleep(5);
   return ErrorAbort(state, kRebootFailure, "%s() failed to reboot", name);
@@ -964,7 +881,12 @@
   if (!android::base::ParseUint(len_str.c_str(), &len)) {
     return nullptr;
   }
-  unique_fd fd(ota_open(filename.c_str(), O_WRONLY, 0644));
+  android::base::unique_fd fd(open(filename.c_str(), O_WRONLY));
+  if (fd == -1) {
+    PLOG(ERROR) << "Failed to open " << filename;
+    return StringValue("");
+  }
+
   // The wipe_block_device function in ext4_utils returns 0 on success and 1
   // for failure.
   int status = wipe_block_device(fd, len);
@@ -1022,14 +944,13 @@
   RegisterFunction("getprop", GetPropFn);
   RegisterFunction("file_getprop", FileGetPropFn);
 
-  RegisterFunction("apply_patch", ApplyPatchFn);
-  RegisterFunction("apply_patch_check", ApplyPatchCheckFn);
   RegisterFunction("apply_patch_space", ApplyPatchSpaceFn);
+  RegisterFunction("patch_partition", PatchPartitionFn);
+  RegisterFunction("patch_partition_check", PatchPartitionCheckFn);
 
   RegisterFunction("wipe_block_device", WipeBlockDeviceFn);
 
   RegisterFunction("read_file", ReadFileFn);
-  RegisterFunction("sha1_check", Sha1CheckFn);
   RegisterFunction("write_value", WriteValueFn);
 
   RegisterFunction("wipe_cache", WipeCacheFn);
diff --git a/updater/updater.cpp b/updater/updater.cpp
index 1d6b172..e87c57a 100644
--- a/updater/updater.cpp
+++ b/updater/updater.cpp
@@ -17,9 +17,9 @@
 #include "updater/updater.h"
 
 #include <stdio.h>
-#include <unistd.h>
 #include <stdlib.h>
 #include <string.h>
+#include <unistd.h>
 
 #include <string>
 
@@ -31,11 +31,9 @@
 #include <ziparchive/zip_archive.h>
 
 #include "edify/expr.h"
-#include "otafault/config.h"
-#include "otautil/DirUtil.h"
-#include "otautil/SysUtil.h"
-#include "otautil/cache_location.h"
+#include "otautil/dirutil.h"
 #include "otautil/error_code.h"
+#include "otautil/sysutil.h"
 #include "updater/blockimg.h"
 #include "updater/install.h"
 
@@ -48,8 +46,6 @@
 // (Note it's "updateR-script", not the older "update-script".)
 static constexpr const char* SCRIPT_NAME = "META-INF/com/google/android/updater-script";
 
-extern bool have_eio_error;
-
 struct selabel_handle *sehandle;
 
 static void UpdaterLogger(android::base::LogId /* id */, android::base::LogSeverity /* severity */,
@@ -135,7 +131,7 @@
 
   std::unique_ptr<Expr> root;
   int error_count = 0;
-  int error = parse_string(script.c_str(), &root, &error_count);
+  int error = ParseString(script, &root, &error_count);
   if (error != 0 || error_count > 0) {
     LOG(ERROR) << error_count << " parse errors";
     CloseArchive(za);
@@ -167,15 +163,10 @@
       printf("unexpected argument: %s", argv[4]);
     }
   }
-  ota_io_init(za, state.is_retry);
 
   std::string result;
   bool status = Evaluate(&state, root, &result);
 
-  if (have_eio_error) {
-    fprintf(cmd_pipe, "retry_update\n");
-  }
-
   if (!status) {
     if (state.errmsg.empty()) {
       LOG(ERROR) << "script aborted (no error message)";
@@ -207,6 +198,9 @@
       if (state.cause_code == kPatchApplicationFailure) {
         LOG(INFO) << "Patch application failed, retry update.";
         fprintf(cmd_pipe, "retry_update\n");
+      } else if (state.cause_code == kEioFailure) {
+        LOG(INFO) << "Update failed due to EIO, retry update.";
+        fprintf(cmd_pipe, "retry_update\n");
       }
     }
 
diff --git a/updater_sample/.gitignore b/updater_sample/.gitignore
new file mode 100644
index 0000000..f846472
--- /dev/null
+++ b/updater_sample/.gitignore
@@ -0,0 +1,10 @@
+*~
+*.bak
+*.pyc
+*.pyc-2.4
+Thumbs.db
+*.iml
+.idea/
+gen/
+.vscode
+local.properties
diff --git a/updater_sample/Android.bp b/updater_sample/Android.bp
new file mode 100644
index 0000000..845e07b
--- /dev/null
+++ b/updater_sample/Android.bp
@@ -0,0 +1,33 @@
+// 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.
+
+android_app {
+    name: "SystemUpdaterSample",
+    sdk_version: "system_current",
+    privileged: true,
+
+    srcs: ["src/**/*.java"],
+
+    static_libs: [
+        "guava",
+    ],
+
+    optimize: {
+        proguard_flags_files: [
+            "proguard.flags",
+        ],
+    },
+
+    resource_dirs: ["res"],
+}
diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml
new file mode 100644
index 0000000..18d8425
--- /dev/null
+++ b/updater_sample/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  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.
+  -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.example.android.systemupdatersample">
+
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
+
+    <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
+
+    <application
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round">
+        <activity
+            android:name=".ui.MainActivity"
+            android:label="@string/app_name" >
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+        <service android:name=".services.PrepareStreamingService"/>
+    </application>
+
+</manifest>
diff --git a/updater_sample/OWNERS b/updater_sample/OWNERS
new file mode 100644
index 0000000..5c1c370
--- /dev/null
+++ b/updater_sample/OWNERS
@@ -0,0 +1,2 @@
+zhaojiac@google.com
+zhomart@google.com
diff --git a/updater_sample/README.md b/updater_sample/README.md
new file mode 100644
index 0000000..f9c3fb8
--- /dev/null
+++ b/updater_sample/README.md
@@ -0,0 +1,267 @@
+# SystemUpdaterSample
+
+This app demonstrates how to use Android system updates APIs to install
+[OTA updates](https://source.android.com/devices/tech/ota/). It contains a
+sample client for `update_engine` to install A/B (seamless) updates.
+
+A/B (seamless) update is available since Android Nougat (API 24), but this sample
+targets the latest android.
+
+
+## Workflow
+
+SystemUpdaterSample app shows list of available updates on the UI. User is allowed
+to select an update and apply it to the device. App shows installation progress,
+logs can be found in `adb logcat`. User can stop or reset an update. Resetting
+the update requests update engine to cancel any ongoing update, and revert
+if the update has been applied. Stopping does not revert the applied update.
+
+
+## Update Config file
+
+In this sample updates are defined in JSON update config files.
+The structure of a config file is defined in
+`com.example.android.systemupdatersample.UpdateConfig`, example file is located
+at `res/raw/sample.json`.
+
+In real-life update system the config files expected to be served from a server
+to the app, but in this sample, the config files are stored on the device.
+The directory can be found in logs or on the UI. In most cases it should be located at
+`/data/user/0/com.example.android.systemupdatersample/files/configs/`.
+
+SystemUpdaterSample app downloads OTA package from `url`. In this sample app
+`url` is expected to point to file system, e.g. `file:///data/my-sample-ota-builds-dir/ota-002.zip`.
+
+If `ab_install_type` is `NON_STREAMING` then app checks if `url` starts
+with `file://` and passes `url` to the `update_engine`.
+
+If `ab_install_type` is `STREAMING`, app downloads only the entries in need, as
+opposed to the entire package, to initiate a streaming update. The `payload.bin`
+entry, which takes up the majority of the space in an OTA package, will be
+streamed by `update_engine` directly. The ZIP entries in such a package need to be
+saved uncompressed (`ZIP_STORED`), so that their data can be downloaded directly
+with the offset and length. As `payload.bin` itself is already in compressed
+format, the size penalty is marginal.
+
+if `ab_config.force_switch_slot` set true device will boot to the
+updated partition on next reboot; otherwise button "Switch Slot" will
+become active, and user can manually set updated partition as the active slot.
+
+Config files can be generated using `tools/gen_update_config.py`.
+Running `./tools/gen_update_config.py --help` shows usage of the script.
+
+
+## Sample App State vs UpdateEngine Status
+
+UpdateEngine provides status for different stages of update application
+process. But it lacks of proper status codes when update fails.
+
+This creates two problems:
+
+1. If sample app is unbound from update_engine (MainActivity is paused, destroyed),
+   app doesn't receive onStatusUpdate and onPayloadApplicationCompleted notifications.
+   If app binds to update_engine after update is completed,
+   only onStatusUpdate is called, but status becomes IDLE in most cases.
+   And there is no way to know if update was successful or not.
+
+2. This sample app demostrates suspend/resume using update_engins's
+   `cancel` and `applyPayload` (which picks up from where it left).
+   When `cancel` is called, status is set to `IDLE`, which doesn't allow
+   tracking suspended state properly.
+
+To solve these problems sample app implements its own separate update
+state - `UpdaterState`. To solve the first problem, sample app persists
+`UpdaterState` on a device. When app is resumed, it checks if `UpdaterState`
+matches the update_engine's status (as onStatusUpdate is guaranteed to be called).
+If they doesn't match, sample app calls `applyPayload` again with the same
+parameters, and handles update completion properly using `onPayloadApplicationCompleted`
+callback. The second problem is solved by adding `PAUSED` updater state.
+
+
+## Sample App UI
+
+### Text fields
+
+- `Current Build:` - shows current active build.
+- `Updater state:` - SystemUpdaterSample app state.
+- `Engine status:` - last reported update_engine status.
+- `Engine error:` - last reported payload application error.
+
+### Buttons
+
+- `Reload` - reloads update configs from device storage.
+- `View config` - shows selected update config.
+- `Apply` - applies selected update config.
+- `Stop` - cancel running update, calls `UpdateEngine#cancel`.
+- `Reset` - reset update, calls `UpdateEngine#resetStatus`, can be called
+            only when update is not running.
+- `Suspend` - suspend running update, uses `UpdateEngine#cancel`.
+- `Resume` - resumes suspended update, uses `UpdateEngine#applyPayload`.
+- `Switch Slot` - if `ab_config.force_switch_slot` config set true,
+            this button will be enabled after payload is applied,
+            to switch A/B slot on next reboot.
+
+
+## Sending HTTP headers from UpdateEngine
+
+Sometimes OTA package server might require some HTTP headers to be present,
+e.g. `Authorization` header to contain valid auth token. While performing
+streaming update, `UpdateEngine` allows passing on certain HTTP headers;
+as of writing this sample app, these headers are `Authorization` and `User-Agent`.
+
+`android.os.UpdateEngine#applyPayload` contains information on
+which HTTP headers are supported.
+
+
+## Used update_engine APIs
+
+### UpdateEngine#bind
+
+Binds given callbacks to update_engine. When update_engine successfully
+initialized, it's guaranteed to invoke callback onStatusUpdate.
+
+### UpdateEngine#applyPayload
+
+Start an update attempt to download an apply the provided `payload_url` if
+no other update is running. The extra `key_value_pair_headers` will be
+included when fetching the payload.
+
+`key_value_pair_headers` argument also accepts properties other than HTTP Headers.
+List of allowed properties can be found in `system/update_engine/common/constants.cc`.
+
+### UpdateEngine#cancel
+
+Cancel the ongoing update. The update could be running or suspended, but it
+can't be canceled after it was done.
+
+### UpdateEngine#resetStatus
+
+Reset the already applied update back to an idle state. This method can
+only be called when no update attempt is going on, and it will reset the
+status back to idle, deleting the currently applied update if any.
+
+### Callback: onStatusUpdate
+
+Called whenever the value of `status` or `progress` changes. For
+`progress` values changes, this method will be called only if it changes significantly.
+At this time of writing this doc, delta for `progress` is `0.005`.
+
+`onStatusUpdate` is always called when app binds to update_engine,
+except when update_engine fails to initialize.
+
+### Callback: onPayloadApplicationComplete
+
+Called whenever an update attempt is completed or failed.
+
+
+## Running on a device
+
+The commands are expected to be run from `$ANDROID_BUILD_TOP` and for demo
+purpose only.
+
+### Without the privileged system permissions
+
+1. Compile the app `mmma -j bootable/recovery/updater_sample`.
+2. Install the app to the device using `adb install <APK_PATH>`.
+3. Change permissions on `/data/ota_package/` to `0777` on the device.
+4. Set SELinux mode to permissive. See instructions below.
+5. Add update config files; look above at [Update Config file](#Update-Config-file).
+6. Push OTA packages to the device.
+7. Run the sample app.
+
+### With the privileged system permissions
+
+To run sample app as a privileged system app, it needs to be installed in `/system/priv-app/`.
+This directory is expected to be read-only, unless explicitly remounted.
+
+The recommended way to run the app is to build and install it as a
+privileged system app, so it's granted the required permissions to access
+`update_engine` service as well as OTA package files. Detailed steps are as follows:
+
+1. [Prepare to build](https://source.android.com/setup/build/building)
+2. Add the module (SystemUpdaterSample) to the `PRODUCT_PACKAGES` list for the
+   lunch target.
+   e.g. add a line containing `PRODUCT_PACKAGES += SystemUpdaterSample`
+   to `device/google/marlin/device-common.mk`.
+3. [Whitelist the sample app](https://source.android.com/devices/tech/config/perms-whitelist)
+   * Add
+   ```
+    <privapp-permissions package="com.example.android.systemupdatersample">
+        <permission name="android.permission.ACCESS_CACHE_FILESYSTEM"/>
+    </privapp-permissions>
+   ```
+   to `frameworks/base/data/etc/privapp-permissions-platform.xml`
+5. Build sample app `mmma -j bootable/recovery/updater_sample`.
+6. Build Android `make -j`
+7. [Flash the device](https://source.android.com/setup/build/running)
+8. Add update config files; look above at `## Update Config file`;
+   `adb root` might be required.
+9. Push OTA packages to the device if there is no server to stream packages from;
+   changing of SELinux labels of OTA packages directory might be required
+   `chcon -R u:object_r:ota_package_file:s0 /data/my-sample-ota-builds-dir`
+10. Run the sample app.
+
+
+## Development
+
+- [x] Create a UI with list of configs, current version,
+      control buttons, progress bar and log viewer
+- [x] Add `PayloadSpec` and `PayloadSpecs` for working with
+      update zip file
+- [x] Add `UpdateConfig` for working with json config files
+- [x] Add applying non-streaming update
+- [x] Prepare streaming update (partially downloading package)
+- [x] Add applying streaming update
+- [x] Add stop/reset the update
+- [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload`
+- [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules)
+- [x] Deferred switch slot demo
+- [x] Add UpdateManager; extract update logic from MainActivity
+- [x] Add Sample app update state (separate from update_engine status)
+- [x] Add smart update completion detection using onStatusUpdate
+- [x] Add pause/resume demo
+- [x] Verify system partition checksum for package
+
+
+## Running tests
+
+1. Build `mmma bootable/recovery/updater_sample/`
+2. Install app
+   `adb install $OUT/system/priv-app/SystemUpdaterSample/SystemUpdaterSample.apk`
+3. Install tests
+   `adb install $OUT/testcases/SystemUpdaterSampleTests/SystemUpdaterSampleTests.apk`
+4. Run tests
+   `adb shell am instrument -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner`
+5. Run a test file
+   ```
+   adb shell am instrument \
+     -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \
+     -c com.example.android.systemupdatersample.util.PayloadSpecsTest
+   ```
+
+
+## Accessing `android.os.UpdateEngine` API
+
+`android.os.UpdateEngine` APIs are marked as `@SystemApi`, meaning only system
+apps can access them.
+
+
+## Getting read/write access to `/data/ota_package/`
+
+Access to cache filesystem is granted only to system apps.
+
+
+## Setting SELinux mode to permissive (0)
+
+```txt
+local$ adb root
+local$ adb shell
+android# setenforce 0
+android# getenforce
+```
+
+
+## License
+
+SystemUpdaterSample app is released under
+[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/tools/dumpkey/Android.mk b/updater_sample/proguard.flags
similarity index 60%
rename from tools/dumpkey/Android.mk
rename to updater_sample/proguard.flags
index 3154914..97ab534 100644
--- a/tools/dumpkey/Android.mk
+++ b/updater_sample/proguard.flags
@@ -1,4 +1,4 @@
-# Copyright (C) 2008 The Android Open Source Project
+# 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.
@@ -12,11 +12,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-LOCAL_PATH := $(call my-dir)
+# Keep, used in tests.
+-keep public class com.example.android.systemupdatersample.UpdateManager {
+   public int getUpdaterState();
+}
 
-include $(CLEAR_VARS)
-LOCAL_MODULE := dumpkey
-LOCAL_SRC_FILES := DumpPublicKey.java
-LOCAL_JAR_MANIFEST := DumpPublicKey.mf
-LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host
-include $(BUILD_HOST_JAVA_LIBRARY)
+# Keep, used in tests.
+-keep public class com.example.android.systemupdatersample.UpdateConfig {
+   public <init>(java.lang.String, java.lang.String, int);
+}
+
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
new file mode 100644
index 0000000..b560827
--- /dev/null
+++ b/updater_sample/res/layout/activity_main.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:orientation="vertical"
+    android:padding="4dip"
+    android:gravity="center_horizontal"
+    android:layout_width="fill_parent"
+    android:layout_height="fill_parent">
+
+    <ScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:layout_marginBottom="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        >
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/textViewBuildtitle"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Current Build:" />
+
+            <TextView
+                android:id="@+id/textViewBuild"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/unknown" />
+
+            <Space
+                android:layout_width="match_parent"
+                android:layout_height="40dp" />
+
+            <TextView
+                android:id="@+id/textView4"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="Apply an update" />
+
+            <TextView
+                android:id="@+id/textViewConfigsDirHint"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:text="Config files located in NULL"
+                android:textColor="#777"
+                android:textSize="10sp"
+                android:textStyle="italic" />
+
+            <Spinner
+                android:id="@+id/spinnerConfigs"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="8dp"
+                android:orientation="horizontal">
+
+                <Button
+                    android:id="@+id/buttonReload"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onReloadClick"
+                    android:text="Reload" />
+
+                <Button
+                    android:id="@+id/buttonViewConfig"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onViewConfigClick"
+                    android:text="View config" />
+
+                <Button
+                    android:id="@+id/buttonApplyConfig"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onApplyConfigClick"
+                    android:text="Apply" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="24dp"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:id="@+id/textView3"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Updater state:" />
+
+                <TextView
+                    android:id="@+id/textViewUpdaterState"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:text="@string/unknown" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:id="@+id/textView"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Engine status:" />
+
+                <TextView
+                    android:id="@+id/textViewEngineStatus"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:text="@string/unknown" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:orientation="horizontal">
+
+                <TextView
+                    android:id="@+id/textView2"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:text="Engine error:" />
+
+                <TextView
+                    android:id="@+id/textViewEngineErrorCode"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="8dp"
+                    android:text="@string/unknown" />
+            </LinearLayout>
+
+
+            <ProgressBar
+                android:id="@+id/progressBar"
+                style="?android:attr/progressBarStyleHorizontal"
+                android:layout_marginTop="8dp"
+                android:min="0"
+                android:max="100"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" />
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="12dp"
+                android:orientation="horizontal">
+
+                <Button
+                    android:id="@+id/buttonStop"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onStopClick"
+                    android:text="Stop" />
+
+                <Button
+                    android:id="@+id/buttonReset"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onResetClick"
+                    android:text="Reset" />
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="12dp"
+                android:orientation="horizontal">
+
+                <Button
+                    android:id="@+id/buttonSuspend"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onSuspendClick"
+                    android:text="Suspend" />
+
+                <Button
+                    android:id="@+id/buttonResume"
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:onClick="onResumeClick"
+                    android:text="Resume" />
+            </LinearLayout>
+
+            <TextView
+                android:id="@+id/textViewUpdateInfo"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="14dp"
+                android:textColor="#777"
+                android:textSize="10sp"
+                android:textStyle="italic"
+                android:text="@string/finish_update_info" />
+
+            <Button
+                android:id="@+id/buttonSwitchSlot"
+                android:layout_width="wrap_content"
+                android:layout_height="match_parent"
+                android:onClick="onSwitchSlotClick"
+                android:text="@string/switch_slot" />
+
+        </LinearLayout>
+
+    </ScrollView>
+
+</LinearLayout>
diff --git a/updater_sample/res/mipmap-hdpi/ic_launcher.png b/updater_sample/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
--- /dev/null
+++ b/updater_sample/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/updater_sample/res/mipmap-hdpi/ic_launcher_round.png b/updater_sample/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
--- /dev/null
+++ b/updater_sample/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json
new file mode 100644
index 0000000..f188c23
--- /dev/null
+++ b/updater_sample/res/raw/sample.json
@@ -0,0 +1,29 @@
+{
+    "__name": "name will be visible on UI",
+    "__url": "https:// or file:// uri to update package (zip, xz, ...)",
+    "__ab_install_type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
+    "name": "SAMPLE-cake-release BUILD-12345",
+    "url": "http://foo.bar/builds/ota-001.zip",
+    "ab_install_type": "NON_STREAMING",
+    "ab_streaming_metadata": {
+        "__": "streaming_metadata is required only for streaming update",
+        "__property_files": "name, offset and size of files",
+        "__authorization": "it will be sent to OTA package server as value of HTTP header - Authorization",
+        "property_files": [
+            {
+                "__filename": "name of the file in package",
+                "__offset": "defines beginning of the file in package",
+                "__size": "size of the file in package",
+                "filename": "payload.bin",
+                "offset": 531,
+                "size": 5012323
+            }
+        ],
+        "authorization": "Basic my-secret-token"
+    },
+    "ab_config": {
+        "__": "A/B (seamless) update configurations",
+        "__force_switch_slot": "if set true device will boot to a new slot, otherwise user manually switches slot on the screen",
+        "force_switch_slot": false
+    }
+}
diff --git a/updater_sample/res/values/strings.xml b/updater_sample/res/values/strings.xml
new file mode 100644
index 0000000..db4a5dc
--- /dev/null
+++ b/updater_sample/res/values/strings.xml
@@ -0,0 +1,23 @@
+<!-- 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.
+-->
+
+<resources>
+    <string name="app_name">SystemUpdaterSample</string>
+    <string name="action_reload">Reload</string>
+    <string name="unknown">Unknown</string>
+    <string name="close">CLOSE</string>
+    <string name="switch_slot">Switch Slot</string>
+    <string name="finish_update_info">To finish the update press the button below</string>
+</resources>
diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
new file mode 100644
index 0000000..ce88338
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.UpdateEngine;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Payload that will be given to {@link UpdateEngine#applyPayload)}.
+ */
+public class PayloadSpec implements Serializable {
+
+    private static final long serialVersionUID = 41043L;
+
+    /**
+     * Creates a payload spec {@link Builder}
+     */
+    public static Builder newBuilder() {
+        return new Builder();
+    }
+
+    private String mUrl;
+    private long mOffset;
+    private long mSize;
+    private List<String> mProperties;
+
+    public PayloadSpec(Builder b) {
+        this.mUrl = b.mUrl;
+        this.mOffset = b.mOffset;
+        this.mSize = b.mSize;
+        this.mProperties = b.mProperties;
+    }
+
+    public String getUrl() {
+        return mUrl;
+    }
+
+    public long getOffset() {
+        return mOffset;
+    }
+
+    public long getSize() {
+        return mSize;
+    }
+
+    public List<String> getProperties() {
+        return mProperties;
+    }
+
+    /**
+     * payload spec builder.
+     *
+     * <p>Usage:</p>
+     *
+     * {@code
+     *   PayloadSpec spec = PayloadSpec.newBuilder()
+     *     .url("url")
+     *     .build();
+     * }
+     */
+    public static class Builder {
+        private String mUrl;
+        private long mOffset;
+        private long mSize;
+        private List<String> mProperties;
+
+        public Builder() {
+        }
+
+        /**
+         * set url
+         */
+        public Builder url(String url) {
+            this.mUrl = url;
+            return this;
+        }
+
+        /**
+         * set offset
+         */
+        public Builder offset(long offset) {
+            this.mOffset = offset;
+            return this;
+        }
+
+        /**
+         * set size
+         */
+        public Builder size(long size) {
+            this.mSize = size;
+            return this;
+        }
+
+        /**
+         * set properties
+         */
+        public Builder properties(List<String> properties) {
+            this.mProperties = properties;
+            return this;
+        }
+
+        /**
+         * build {@link PayloadSpec}
+         */
+        public PayloadSpec build() {
+            return new PayloadSpec(this);
+        }
+    }
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
new file mode 100644
index 0000000..61872a6
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * An update description. It will be parsed from JSON, which is intended to
+ * be sent from server to the update app, but in this sample app it will be stored on the device.
+ */
+public class UpdateConfig implements Parcelable {
+
+    public static final int AB_INSTALL_TYPE_NON_STREAMING = 0;
+    public static final int AB_INSTALL_TYPE_STREAMING = 1;
+
+    public static final Parcelable.Creator<UpdateConfig> CREATOR =
+            new Parcelable.Creator<UpdateConfig>() {
+                @Override
+                public UpdateConfig createFromParcel(Parcel source) {
+                    return new UpdateConfig(source);
+                }
+
+                @Override
+                public UpdateConfig[] newArray(int size) {
+                    return new UpdateConfig[size];
+                }
+            };
+
+    /** parse update config from json */
+    public static UpdateConfig fromJson(String json) throws JSONException {
+        UpdateConfig c = new UpdateConfig();
+
+        JSONObject o = new JSONObject(json);
+        c.mName = o.getString("name");
+        c.mUrl = o.getString("url");
+        switch (o.getString("ab_install_type")) {
+            case AB_INSTALL_TYPE_NON_STREAMING_JSON:
+                c.mAbInstallType = AB_INSTALL_TYPE_NON_STREAMING;
+                break;
+            case AB_INSTALL_TYPE_STREAMING_JSON:
+                c.mAbInstallType = AB_INSTALL_TYPE_STREAMING;
+                break;
+            default:
+                throw new JSONException("Invalid type, expected either "
+                        + "NON_STREAMING or STREAMING, got " + o.getString("ab_install_type"));
+        }
+
+        // TODO: parse only for A/B updates when non-A/B is implemented
+        JSONObject ab = o.getJSONObject("ab_config");
+        boolean forceSwitchSlot = ab.getBoolean("force_switch_slot");
+        boolean verifyPayloadMetadata = ab.getBoolean("verify_payload_metadata");
+        ArrayList<PackageFile> propertyFiles = new ArrayList<>();
+        if (ab.has("property_files")) {
+            JSONArray propertyFilesJson = ab.getJSONArray("property_files");
+            for (int i = 0; i < propertyFilesJson.length(); i++) {
+                JSONObject p = propertyFilesJson.getJSONObject(i);
+                propertyFiles.add(new PackageFile(
+                        p.getString("filename"),
+                        p.getLong("offset"),
+                        p.getLong("size")));
+            }
+        }
+        String authorization = ab.optString("authorization", null);
+        c.mAbConfig = new AbConfig(
+                forceSwitchSlot,
+                verifyPayloadMetadata,
+                propertyFiles.toArray(new PackageFile[0]),
+                authorization);
+
+        c.mRawJson = json;
+        return c;
+    }
+
+    /**
+     * these strings are represent types in JSON config files
+     */
+    private static final String AB_INSTALL_TYPE_NON_STREAMING_JSON = "NON_STREAMING";
+    private static final String AB_INSTALL_TYPE_STREAMING_JSON = "STREAMING";
+
+    /** name will be visible on UI */
+    private String mName;
+
+    /** update zip file URI, can be https:// or file:// */
+    private String mUrl;
+
+    /** non-streaming (first saves locally) OR streaming (on the fly) */
+    private int mAbInstallType;
+
+    /** A/B update configurations */
+    private AbConfig mAbConfig;
+
+    private String mRawJson;
+
+    protected UpdateConfig() {
+    }
+
+    protected UpdateConfig(Parcel in) {
+        this.mName = in.readString();
+        this.mUrl = in.readString();
+        this.mAbInstallType = in.readInt();
+        this.mAbConfig = (AbConfig) in.readSerializable();
+        this.mRawJson = in.readString();
+    }
+
+    public UpdateConfig(String name, String url, int installType) {
+        this.mName = name;
+        this.mUrl = url;
+        this.mAbInstallType = installType;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public String getUrl() {
+        return mUrl;
+    }
+
+    public String getRawJson() {
+        return mRawJson;
+    }
+
+    public int getInstallType() {
+        return mAbInstallType;
+    }
+
+    public AbConfig getAbConfig() {
+        return mAbConfig;
+    }
+
+    /**
+     * @return File object for given url
+     */
+    public File getUpdatePackageFile() {
+        if (mAbInstallType != AB_INSTALL_TYPE_NON_STREAMING) {
+            throw new RuntimeException("Expected non-streaming install type");
+        }
+        if (!mUrl.startsWith("file://")) {
+            throw new RuntimeException("url is expected to start with file://");
+        }
+        return new File(mUrl.substring(7, mUrl.length()));
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mName);
+        dest.writeString(mUrl);
+        dest.writeInt(mAbInstallType);
+        dest.writeSerializable(mAbConfig);
+        dest.writeString(mRawJson);
+    }
+
+    /**
+     * Description of a file in an OTA package zip file.
+     */
+    public static class PackageFile implements Serializable {
+
+        private static final long serialVersionUID = 31043L;
+
+        /** filename in an archive */
+        private String mFilename;
+
+        /** defines beginning of update data in archive */
+        private long mOffset;
+
+        /** size of the update data in archive */
+        private long mSize;
+
+        public PackageFile(String filename, long offset, long size) {
+            this.mFilename = filename;
+            this.mOffset = offset;
+            this.mSize = size;
+        }
+
+        public String getFilename() {
+            return mFilename;
+        }
+
+        public long getOffset() {
+            return mOffset;
+        }
+
+        public long getSize() {
+            return mSize;
+        }
+    }
+
+    /**
+     * A/B (seamless) update configurations.
+     */
+    public static class AbConfig implements Serializable {
+
+        private static final long serialVersionUID = 31044L;
+
+        /**
+         * if set true device will boot to new slot, otherwise user manually
+         * switches slot on the screen.
+         */
+        private boolean mForceSwitchSlot;
+
+        /**
+         * if set true device will boot to new slot, otherwise user manually
+         * switches slot on the screen.
+         */
+        private boolean mVerifyPayloadMetadata;
+
+        /** defines beginning of update data in archive */
+        private PackageFile[] mPropertyFiles;
+
+        /**
+         * SystemUpdaterSample receives the authorization token from the OTA server, in addition
+         * to the package URL. It passes on the info to update_engine, so that the latter can
+         * fetch the data from the package server directly with the token.
+         */
+        private String mAuthorization;
+
+        public AbConfig(
+                boolean forceSwitchSlot,
+                boolean verifyPayloadMetadata,
+                PackageFile[] propertyFiles,
+                String authorization) {
+            this.mForceSwitchSlot = forceSwitchSlot;
+            this.mVerifyPayloadMetadata = verifyPayloadMetadata;
+            this.mPropertyFiles = propertyFiles;
+            this.mAuthorization = authorization;
+        }
+
+        public boolean getForceSwitchSlot() {
+            return mForceSwitchSlot;
+        }
+
+        public boolean getVerifyPayloadMetadata() {
+            return mVerifyPayloadMetadata;
+        }
+
+        public PackageFile[] getPropertyFiles() {
+            return mPropertyFiles;
+        }
+
+        public Optional<String> getAuthorization() {
+            return mAuthorization == null ? Optional.empty() : Optional.of(mAuthorization);
+        }
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
new file mode 100644
index 0000000..12a8f3f
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.content.Context;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.services.PrepareStreamingService;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
+import com.example.android.systemupdatersample.util.UpdateEngineProperties;
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.AtomicDouble;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Manages the update flow. It has its own state (in memory), separate from
+ * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
+ */
+public class UpdateManager {
+
+    private static final String TAG = "UpdateManager";
+
+    /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */
+    private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+            + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
+
+    private final UpdateEngine mUpdateEngine;
+    private final PayloadSpecs mPayloadSpecs;
+
+    private AtomicInteger mUpdateEngineStatus =
+            new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
+    private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN);
+    private AtomicDouble mProgress = new AtomicDouble(0);
+    private UpdaterState mUpdaterState = new UpdaterState(UpdaterState.IDLE);
+
+    private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
+
+    /** Synchronize state with engine status only once when app binds to UpdateEngine. */
+    private AtomicBoolean mStateSynchronized = new AtomicBoolean(false);
+
+    @GuardedBy("mLock")
+    private UpdateData mLastUpdateData = null;
+
+    @GuardedBy("mLock")
+    private IntConsumer mOnStateChangeCallback = null;
+    @GuardedBy("mLock")
+    private IntConsumer mOnEngineStatusUpdateCallback = null;
+    @GuardedBy("mLock")
+    private DoubleConsumer mOnProgressUpdateCallback = null;
+    @GuardedBy("mLock")
+    private IntConsumer mOnEngineCompleteCallback = null;
+
+    private final Object mLock = new Object();
+
+    private final UpdateManager.UpdateEngineCallbackImpl
+            mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
+
+    public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) {
+        this.mUpdateEngine = updateEngine;
+        this.mPayloadSpecs = payloadSpecs;
+    }
+
+    /**
+     * Binds to {@link UpdateEngine}. Invokes onStateChangeCallback if present.
+     */
+    public void bind() {
+        getOnStateChangeCallback().ifPresent(callback -> callback.accept(mUpdaterState.get()));
+
+        mStateSynchronized.set(false);
+        this.mUpdateEngine.bind(mUpdateEngineCallback);
+    }
+
+    /**
+     * Unbinds from {@link UpdateEngine}.
+     */
+    public void unbind() {
+        this.mUpdateEngine.unbind();
+    }
+
+    public int getUpdaterState() {
+        return mUpdaterState.get();
+    }
+
+    /**
+     * Returns true if manual switching slot is required. Value depends on
+     * the update config {@code ab_config.force_switch_slot}.
+     */
+    public boolean isManualSwitchSlotRequired() {
+        return mManualSwitchSlotRequired.get();
+    }
+
+    /**
+     * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
+     * of the values from {@link UpdaterState}.
+     *
+     * @param onStateChangeCallback a callback with parameter {@code state}.
+     */
+    public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
+        synchronized (mLock) {
+            this.mOnStateChangeCallback = onStateChangeCallback;
+        }
+    }
+
+    private Optional<IntConsumer> getOnStateChangeCallback() {
+        synchronized (mLock) {
+            return mOnStateChangeCallback == null
+                    ? Optional.empty()
+                    : Optional.of(mOnStateChangeCallback);
+        }
+    }
+
+    /**
+     * Sets update engine status update callback. Value of {@code status} will
+     * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
+     *
+     * @param onStatusUpdateCallback a callback with parameter {@code status}.
+     */
+    public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) {
+        synchronized (mLock) {
+            this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback;
+        }
+    }
+
+    private Optional<IntConsumer> getOnEngineStatusUpdateCallback() {
+        synchronized (mLock) {
+            return mOnEngineStatusUpdateCallback == null
+                    ? Optional.empty()
+                    : Optional.of(mOnEngineStatusUpdateCallback);
+        }
+    }
+
+    /**
+     * Sets update engine payload application complete callback. Value of {@code errorCode} will
+     * be one of the values from {@link UpdateEngine.ErrorCodeConstants}.
+     *
+     * @param onComplete a callback with parameter {@code errorCode}.
+     */
+    public void setOnEngineCompleteCallback(IntConsumer onComplete) {
+        synchronized (mLock) {
+            this.mOnEngineCompleteCallback = onComplete;
+        }
+    }
+
+    private Optional<IntConsumer> getOnEngineCompleteCallback() {
+        synchronized (mLock) {
+            return mOnEngineCompleteCallback == null
+                    ? Optional.empty()
+                    : Optional.of(mOnEngineCompleteCallback);
+        }
+    }
+
+    /**
+     * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}.
+     *
+     * @param onProgressCallback a callback with parameter {@code progress}.
+     */
+    public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) {
+        synchronized (mLock) {
+            this.mOnProgressUpdateCallback = onProgressCallback;
+        }
+    }
+
+    private Optional<DoubleConsumer> getOnProgressUpdateCallback() {
+        synchronized (mLock) {
+            return mOnProgressUpdateCallback == null
+                    ? Optional.empty()
+                    : Optional.of(mOnProgressUpdateCallback);
+        }
+    }
+
+    /**
+     * Suspend running update.
+     */
+    public synchronized void suspend() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "suspend invoked");
+        setUpdaterState(UpdaterState.PAUSED);
+        mUpdateEngine.cancel();
+    }
+
+    /**
+     * Resume suspended update.
+     */
+    public synchronized void resume() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "resume invoked");
+        setUpdaterState(UpdaterState.RUNNING);
+        updateEngineReApplyPayload();
+    }
+
+    /**
+     * Updates {@link this.mState} and if state is changed,
+     * it also notifies {@link this.mOnStateChangeCallback}.
+     */
+    private void setUpdaterState(int newUpdaterState)
+            throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "setUpdaterState invoked newState=" + newUpdaterState);
+        int previousState = mUpdaterState.get();
+        mUpdaterState.set(newUpdaterState);
+        if (previousState != newUpdaterState) {
+            getOnStateChangeCallback().ifPresent(callback -> callback.accept(newUpdaterState));
+        }
+    }
+
+    /**
+     * Same as {@link this.setUpdaterState}. Logs the error if new state
+     * cannot be set.
+     */
+    private void setUpdaterStateSilent(int newUpdaterState) {
+        try {
+            setUpdaterState(newUpdaterState);
+        } catch (UpdaterState.InvalidTransitionException e) {
+            // Most likely UpdateEngine status and UpdaterSample state got de-synchronized.
+            // To make sample app simple, we don't handle it properly.
+            Log.e(TAG, "Failed to set updater state", e);
+        }
+    }
+
+    /**
+     * Creates new UpdaterState, assigns it to {@link this.mUpdaterState},
+     * and notifies callbacks.
+     */
+    private void initializeUpdateState(int state) {
+        this.mUpdaterState = new UpdaterState(state);
+        getOnStateChangeCallback().ifPresent(callback -> callback.accept(state));
+    }
+
+    /**
+     * Requests update engine to stop any ongoing update. If an update has been applied,
+     * leave it as is.
+     */
+    public synchronized void cancelRunningUpdate() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "cancelRunningUpdate invoked");
+        setUpdaterState(UpdaterState.IDLE);
+        mUpdateEngine.cancel();
+    }
+
+    /**
+     * Resets update engine to IDLE state. If an update has been applied it reverts it.
+     */
+    public synchronized void resetUpdate() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "resetUpdate invoked");
+        setUpdaterState(UpdaterState.IDLE);
+        mUpdateEngine.resetStatus();
+    }
+
+    /**
+     * Applies the given update.
+     *
+     * <p>UpdateEngine works asynchronously. This method doesn't wait until
+     * end of the update.</p>
+     */
+    public synchronized void applyUpdate(Context context, UpdateConfig config)
+            throws UpdaterState.InvalidTransitionException {
+        mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
+        setUpdaterState(UpdaterState.RUNNING);
+
+        synchronized (mLock) {
+            // Cleaning up previous update data.
+            mLastUpdateData = null;
+        }
+
+        if (!config.getAbConfig().getForceSwitchSlot()) {
+            mManualSwitchSlotRequired.set(true);
+        } else {
+            mManualSwitchSlotRequired.set(false);
+        }
+
+        if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
+            applyAbNonStreamingUpdate(config);
+        } else {
+            applyAbStreamingUpdate(context, config);
+        }
+    }
+
+    private void applyAbNonStreamingUpdate(UpdateConfig config)
+            throws UpdaterState.InvalidTransitionException {
+        UpdateData.Builder builder = UpdateData.builder()
+                .setExtraProperties(prepareExtraProperties(config));
+
+        try {
+            builder.setPayload(mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()));
+        } catch (IOException e) {
+            Log.e(TAG, "Error creating payload spec", e);
+            setUpdaterState(UpdaterState.ERROR);
+            return;
+        }
+        updateEngineApplyPayload(builder.build());
+    }
+
+    private void applyAbStreamingUpdate(Context context, UpdateConfig config) {
+        UpdateData.Builder builder = UpdateData.builder()
+                .setExtraProperties(prepareExtraProperties(config));
+
+        Log.d(TAG, "Starting PrepareStreamingService");
+        PrepareStreamingService.startService(context, config, (code, payloadSpec) -> {
+            if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
+                builder.setPayload(payloadSpec);
+                builder.addExtraProperty("USER_AGENT=" + HTTP_USER_AGENT);
+                config.getAbConfig()
+                        .getAuthorization()
+                        .ifPresent(s -> builder.addExtraProperty("AUTHORIZATION=" + s));
+                updateEngineApplyPayload(builder.build());
+            } else {
+                Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
+                setUpdaterStateSilent(UpdaterState.ERROR);
+            }
+        });
+    }
+
+    private List<String> prepareExtraProperties(UpdateConfig config) {
+        List<String> extraProperties = new ArrayList<>();
+
+        if (!config.getAbConfig().getForceSwitchSlot()) {
+            // Disable switch slot on reboot, which is enabled by default.
+            // User will enable it manually by clicking "Switch Slot" button on the screen.
+            extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
+        }
+        return extraProperties;
+    }
+
+    /**
+     * Applies given payload.
+     *
+     * <p>UpdateEngine works asynchronously. This method doesn't wait until
+     * end of the update.</p>
+     *
+     * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid
+     * payload properties (which come from OTA packages), or failing to set up the network
+     * with the given id.</p>
+     */
+    private void updateEngineApplyPayload(UpdateData update) {
+        Log.d(TAG, "updateEngineApplyPayload invoked with url " + update.mPayload.getUrl());
+
+        synchronized (mLock) {
+            mLastUpdateData = update;
+        }
+
+        ArrayList<String> properties = new ArrayList<>(update.getPayload().getProperties());
+        properties.addAll(update.getExtraProperties());
+
+        try {
+            mUpdateEngine.applyPayload(
+                    update.getPayload().getUrl(),
+                    update.getPayload().getOffset(),
+                    update.getPayload().getSize(),
+                    properties.toArray(new String[0]));
+        } catch (Exception e) {
+            Log.e(TAG, "UpdateEngine failed to apply the update", e);
+            setUpdaterStateSilent(UpdaterState.ERROR);
+        }
+    }
+
+    /**
+     * Re-applies {@link this.mLastUpdateData} to update_engine.
+     */
+    private void updateEngineReApplyPayload() {
+        Log.d(TAG, "updateEngineReApplyPayload invoked");
+        UpdateData lastUpdate;
+        synchronized (mLock) {
+            // mLastPayloadSpec might be empty in some cases.
+            // But to make this sample app simple, we will not handle it.
+            Preconditions.checkArgument(
+                    mLastUpdateData != null,
+                    "mLastUpdateData must be present.");
+            lastUpdate = mLastUpdateData;
+        }
+        updateEngineApplyPayload(lastUpdate);
+    }
+
+    /**
+     * Sets the new slot that has the updated partitions as the active slot,
+     * which device will boot into next time.
+     * This method is only supposed to be called after the payload is applied.
+     *
+     * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
+     * and payload metadata headers doesn't trigger new update. It can be used to just switch
+     * active A/B slot.
+     *
+     * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
+     * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
+     */
+    public synchronized void setSwitchSlotOnReboot() {
+        Log.d(TAG, "setSwitchSlotOnReboot invoked");
+
+        // When mManualSwitchSlotRequired set false, next time
+        // onApplicationPayloadComplete is called,
+        // it will set updater state to REBOOT_REQUIRED.
+        mManualSwitchSlotRequired.set(false);
+
+        UpdateData.Builder builder;
+        synchronized (mLock) {
+            // To make sample app simple, we don't handle it.
+            Preconditions.checkArgument(
+                    mLastUpdateData != null,
+                    "mLastUpdateData must be present.");
+            builder = mLastUpdateData.toBuilder();
+        }
+        // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
+        builder.setExtraProperties(
+                Collections.singletonList(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL));
+        // UpdateEngine sets property SWITCH_SLOT_ON_REBOOT=1 by default.
+        // HTTP headers are not required, UpdateEngine is not expected to stream payload.
+        updateEngineApplyPayload(builder.build());
+    }
+
+    /**
+     * Synchronize UpdaterState with UpdateEngine status.
+     * Apply necessary UpdateEngine operation if status are out of sync.
+     *
+     * It's expected to be called once when sample app binds itself to UpdateEngine.
+     */
+    private void synchronizeUpdaterStateWithUpdateEngineStatus() {
+        Log.d(TAG, "synchronizeUpdaterStateWithUpdateEngineStatus is invoked.");
+
+        int state = mUpdaterState.get();
+        int engineStatus = mUpdateEngineStatus.get();
+
+        if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
+            // If update has been installed before running the sample app,
+            // set state to REBOOT_REQUIRED.
+            initializeUpdateState(UpdaterState.REBOOT_REQUIRED);
+            return;
+        }
+
+        switch (state) {
+            case UpdaterState.IDLE:
+            case UpdaterState.ERROR:
+            case UpdaterState.PAUSED:
+            case UpdaterState.SLOT_SWITCH_REQUIRED:
+                // It might happen when update is started not from the sample app.
+                // To make the sample app simple, we won't handle this case.
+                Preconditions.checkState(
+                        engineStatus == UpdateEngine.UpdateStatusConstants.IDLE,
+                        "When mUpdaterState is %s, mUpdateEngineStatus "
+                                + "must be 0/IDLE, but it is %s",
+                        state,
+                        engineStatus);
+                break;
+            case UpdaterState.RUNNING:
+                if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
+                        || engineStatus == UpdateEngine.UpdateStatusConstants.IDLE) {
+                    Log.i(TAG, "ensureUpdateEngineStatusIsRunning - re-applying last payload");
+                    // Re-apply latest update. It makes update_engine to invoke
+                    // onPayloadApplicationComplete callback. The callback notifies
+                    // if update was successful or not.
+                    updateEngineReApplyPayload();
+                }
+                break;
+            case UpdaterState.REBOOT_REQUIRED:
+                // This might happen when update is installed by other means,
+                // and sample app is not aware of it.
+                // To make the sample app simple, we won't handle this case.
+                Preconditions.checkState(
+                        engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT,
+                        "When mUpdaterState is %s, mUpdateEngineStatus "
+                                + "must be 6/UPDATED_NEED_REBOOT, but it is %s",
+                        state,
+                        engineStatus);
+                break;
+            default:
+                throw new IllegalStateException("This block should not be reached.");
+        }
+    }
+
+    /**
+     * Invoked by update_engine whenever update status or progress changes.
+     * It's also guaranteed to be invoked when app binds to the update_engine, except
+     * when update_engine fails to initialize (as defined in
+     * system/update_engine/binder_service_android.cc in
+     * function BinderUpdateEngineAndroidService::bind).
+     *
+     * @param status one of {@link UpdateEngine.UpdateStatusConstants}.
+     * @param progress a number from 0.0 to 1.0.
+     */
+    private void onStatusUpdate(int status, float progress) {
+        Log.d(TAG, String.format(
+                        "onStatusUpdate invoked, status=%s, progress=%.2f",
+                        status,
+                        progress));
+
+        int previousStatus = mUpdateEngineStatus.get();
+        mUpdateEngineStatus.set(status);
+        mProgress.set(progress);
+
+        if (!mStateSynchronized.getAndSet(true)) {
+            // We synchronize state with engine status once
+            // only when sample app is bound to UpdateEngine.
+            synchronizeUpdaterStateWithUpdateEngineStatus();
+        }
+
+        getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(mProgress.get()));
+
+        if (previousStatus != status) {
+            getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status));
+        }
+    }
+
+    private void onPayloadApplicationComplete(int errorCode) {
+        Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
+        mEngineErrorCode.set(errorCode);
+        if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
+                || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
+            setUpdaterStateSilent(isManualSwitchSlotRequired()
+                    ? UpdaterState.SLOT_SWITCH_REQUIRED
+                    : UpdaterState.REBOOT_REQUIRED);
+        } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
+            setUpdaterStateSilent(UpdaterState.ERROR);
+        }
+
+        getOnEngineCompleteCallback()
+                .ifPresent(callback -> callback.accept(errorCode));
+    }
+
+    /**
+     * Helper class to delegate {@code update_engine} callback invocations to UpdateManager.
+     */
+    class UpdateEngineCallbackImpl extends UpdateEngineCallback {
+        @Override
+        public void onStatusUpdate(int status, float percent) {
+            UpdateManager.this.onStatusUpdate(status, percent);
+        }
+
+        @Override
+        public void onPayloadApplicationComplete(int errorCode) {
+            UpdateManager.this.onPayloadApplicationComplete(errorCode);
+        }
+    }
+
+    /**
+     *
+     * Contains update data - PayloadSpec and extra properties list.
+     *
+     * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}.
+     * {@code mExtraProperties} is a list of additional properties to pass to
+     * {@link UpdateEngine#applyPayload}.</p>
+     */
+    private static class UpdateData {
+        private final PayloadSpec mPayload;
+        private final ImmutableList<String> mExtraProperties;
+
+        public static Builder builder() {
+            return new Builder();
+        }
+
+        UpdateData(Builder builder) {
+            this.mPayload = builder.mPayload;
+            this.mExtraProperties = ImmutableList.copyOf(builder.mExtraProperties);
+        }
+
+        public PayloadSpec getPayload() {
+            return mPayload;
+        }
+
+        public ImmutableList<String> getExtraProperties() {
+            return mExtraProperties;
+        }
+
+        public Builder toBuilder() {
+            return builder()
+                    .setPayload(mPayload)
+                    .setExtraProperties(mExtraProperties);
+        }
+
+        static class Builder {
+            private PayloadSpec mPayload;
+            private List<String> mExtraProperties;
+
+            public Builder setPayload(PayloadSpec payload) {
+                this.mPayload = payload;
+                return this;
+            }
+
+            public Builder setExtraProperties(List<String> extraProperties) {
+                this.mExtraProperties = new ArrayList<>(extraProperties);
+                return this;
+            }
+
+            public Builder addExtraProperty(String property) {
+                if (this.mExtraProperties == null) {
+                    this.mExtraProperties = new ArrayList<>();
+                }
+                this.mExtraProperties.add(property);
+                return this;
+            }
+
+            public UpdateData build() {
+                return new UpdateData(this);
+            }
+        }
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
new file mode 100644
index 0000000..4eb0b68
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Controls updater state.
+ */
+public class UpdaterState {
+
+    public static final int IDLE = 0;
+    public static final int ERROR = 1;
+    public static final int RUNNING = 2;
+    public static final int PAUSED = 3;
+    public static final int SLOT_SWITCH_REQUIRED = 4;
+    public static final int REBOOT_REQUIRED = 5;
+
+    private static final SparseArray<String> STATE_MAP = new SparseArray<>();
+
+    static {
+        STATE_MAP.put(0, "IDLE");
+        STATE_MAP.put(1, "ERROR");
+        STATE_MAP.put(2, "RUNNING");
+        STATE_MAP.put(3, "PAUSED");
+        STATE_MAP.put(4, "SLOT_SWITCH_REQUIRED");
+        STATE_MAP.put(5, "REBOOT_REQUIRED");
+    }
+
+    /**
+     * Allowed state transitions. It's a map: key is a state, value is a set of states that
+     * are allowed to transition to from key.
+     */
+    private static final ImmutableMap<Integer, ImmutableSet<Integer>> TRANSITIONS =
+            ImmutableMap.<Integer, ImmutableSet<Integer>>builder()
+                    .put(IDLE, ImmutableSet.of(IDLE, ERROR, RUNNING))
+                    .put(ERROR, ImmutableSet.of(IDLE))
+                    .put(RUNNING, ImmutableSet.of(
+                            IDLE, ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED))
+                    .put(PAUSED, ImmutableSet.of(ERROR, RUNNING, IDLE))
+                    .put(SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR, REBOOT_REQUIRED, IDLE))
+                    .put(REBOOT_REQUIRED, ImmutableSet.of(IDLE))
+                    .build();
+
+    private AtomicInteger mState;
+
+    public UpdaterState(int state) {
+        this.mState = new AtomicInteger(state);
+    }
+
+    /**
+     * Returns updater state.
+     */
+    public int get() {
+        return mState.get();
+    }
+
+    /**
+     * Sets the updater state.
+     *
+     * @throws InvalidTransitionException if transition is not allowed.
+     */
+    public void set(int newState) throws InvalidTransitionException {
+        int oldState = mState.get();
+        if (!TRANSITIONS.get(oldState).contains(newState)) {
+            throw new InvalidTransitionException(
+                    "Can't transition from " + oldState + " to " + newState);
+        }
+        mState.set(newState);
+    }
+
+    /**
+     * Converts status code to status name.
+     */
+    public static String getStateText(int state) {
+        return STATE_MAP.get(state);
+    }
+
+    /**
+     * Defines invalid state transition exception.
+     */
+    public static class InvalidTransitionException extends Exception {
+        public InvalidTransitionException(String msg) {
+            super(msg);
+        }
+    }
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
new file mode 100644
index 0000000..9314048
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.services;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RecoverySystem;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.util.FileDownloader;
+import com.example.android.systemupdatersample.util.PackageFiles;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.google.common.collect.ImmutableSet;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+/**
+ * This IntentService will download/extract the necessary files from the package zip
+ * without downloading the whole package. And it constructs {@link PayloadSpec}.
+ * All this work required to install streaming A/B updates.
+ *
+ * PrepareStreamingService runs on it's own thread. It will notify activity
+ * using interface {@link UpdateResultCallback} when update is ready to install.
+ */
+public class PrepareStreamingService extends IntentService {
+
+    /**
+     * UpdateResultCallback result codes.
+     */
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final int RESULT_CODE_ERROR = 1;
+
+    /**
+     * This interface is used to send results from {@link PrepareStreamingService} to
+     * {@code MainActivity}.
+     */
+    public interface UpdateResultCallback {
+
+        /**
+         * Invoked when files are downloaded and payload spec is constructed.
+         *
+         * @param resultCode result code, values are defined in {@link PrepareStreamingService}
+         * @param payloadSpec prepared payload spec for streaming update
+         */
+        void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+    }
+
+    /**
+     * Starts PrepareStreamingService.
+     *
+     * @param context application context
+     * @param config update config
+     * @param resultCallback callback that will be called when the update is ready to be installed
+     */
+    public static void startService(Context context,
+            UpdateConfig config,
+            UpdateResultCallback resultCallback) {
+        Log.d(TAG, "Starting PrepareStreamingService");
+        ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback);
+        Intent intent = new Intent(context, PrepareStreamingService.class);
+        intent.putExtra(EXTRA_PARAM_CONFIG, config);
+        intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+        context.startService(intent);
+    }
+
+    public PrepareStreamingService() {
+        super(TAG);
+    }
+
+    private static final String TAG = "PrepareStreamingService";
+
+    /**
+     * Extra params that will be sent from Activity to IntentService.
+     */
+    private static final String EXTRA_PARAM_CONFIG = "config";
+    private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
+
+    /**
+     * The files that should be downloaded before streaming.
+     */
+    private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
+            ImmutableSet.of(
+                PackageFiles.CARE_MAP_FILE_NAME,
+                PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
+                PackageFiles.METADATA_FILE_NAME,
+                PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
+            );
+
+    private final PayloadSpecs mPayloadSpecs = new PayloadSpecs();
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        Log.d(TAG, "On handle intent is called");
+        UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG);
+        ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
+
+        try {
+            PayloadSpec spec = execute(config);
+            resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to prepare streaming update", e);
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+        }
+    }
+
+    /**
+     * 1. Downloads files for streaming updates.
+     * 2. Makes sure required files are present.
+     * 3. Checks OTA package compatibility with the device.
+     * 4. Constructs {@link PayloadSpec} for streaming update.
+     */
+    private PayloadSpec execute(UpdateConfig config)
+            throws IOException, PreparationFailedException {
+
+        downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
+
+        Optional<UpdateConfig.PackageFile> payloadBinary =
+                UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
+
+        if (!payloadBinary.isPresent()) {
+            throw new PreparationFailedException(
+                    "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
+        }
+
+        if (!UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config).isPresent()
+                || !Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile().exists()) {
+            throw new IOException(PAYLOAD_PROPERTIES_FILE_NAME + " not found");
+        }
+
+        File compatibilityFile = Paths.get(OTA_PACKAGE_DIR, COMPATIBILITY_ZIP_FILE_NAME).toFile();
+        if (compatibilityFile.isFile()) {
+            Log.i(TAG, "Verifying OTA package for compatibility with the device");
+            if (!verifyPackageCompatibility(compatibilityFile)) {
+                throw new PreparationFailedException(
+                        "OTA package is not compatible with this device");
+            }
+        }
+
+        return mPayloadSpecs.forStreaming(config.getUrl(),
+                payloadBinary.get().getOffset(),
+                payloadBinary.get().getSize(),
+                Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile());
+    }
+
+    /**
+     * Downloads files defined in {@link UpdateConfig#getAbConfig()}
+     * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
+     * in directory {@code dir}.
+     * @throws IOException when can't download a file
+     */
+    private void downloadPreStreamingFiles(UpdateConfig config, String dir)
+            throws IOException {
+        Log.d(TAG, "Deleting existing files from " + dir);
+        for (String file : PRE_STREAMING_FILES_SET) {
+            Files.deleteIfExists(Paths.get(OTA_PACKAGE_DIR, file));
+        }
+        Log.d(TAG, "Downloading files to " + dir);
+        for (UpdateConfig.PackageFile file : config.getAbConfig().getPropertyFiles()) {
+            if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) {
+                Log.d(TAG, "Downloading file " + file.getFilename());
+                FileDownloader downloader = new FileDownloader(
+                        config.getUrl(),
+                        file.getOffset(),
+                        file.getSize(),
+                        Paths.get(dir, file.getFilename()).toFile());
+                downloader.download();
+            }
+        }
+    }
+
+    /**
+     * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME}
+     * @return true if OTA package is compatible with this device
+     */
+    private boolean verifyPackageCompatibility(File file) {
+        try {
+            return RecoverySystem.verifyPackageCompatibility(file);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to verify package compatibility", e);
+            return false;
+        }
+    }
+
+    /**
+     * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec}
+     * to {@link UpdateResultCallback#onReceiveResult}.
+     */
+    private static class CallbackResultReceiver extends ResultReceiver {
+
+        static Bundle createBundle(PayloadSpec payloadSpec) {
+            Bundle b = new Bundle();
+            b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec);
+            return b;
+        }
+
+        private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec";
+
+        private UpdateResultCallback mUpdateResultCallback;
+
+        CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
+            super(handler);
+            this.mUpdateResultCallback = updateResultCallback;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            PayloadSpec payloadSpec = null;
+            if (resultCode == RESULT_CODE_SUCCESS) {
+                payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC);
+            }
+            mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec);
+        }
+    }
+
+    private static class PreparationFailedException extends Exception {
+        PreparationFailedException(String message) {
+            super(message);
+        }
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
new file mode 100644
index 0000000..fc9fddd
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UpdateEngine;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.example.android.systemupdatersample.R;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.UpdateManager;
+import com.example.android.systemupdatersample.UpdaterState;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
+import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
+
+import java.util.List;
+
+/**
+ * UI for SystemUpdaterSample app.
+ */
+public class MainActivity extends Activity {
+
+    private static final String TAG = "MainActivity";
+
+    private TextView mTextViewBuild;
+    private Spinner mSpinnerConfigs;
+    private TextView mTextViewConfigsDirHint;
+    private Button mButtonReload;
+    private Button mButtonApplyConfig;
+    private Button mButtonStop;
+    private Button mButtonReset;
+    private Button mButtonSuspend;
+    private Button mButtonResume;
+    private ProgressBar mProgressBar;
+    private TextView mTextViewUpdaterState;
+    private TextView mTextViewEngineStatus;
+    private TextView mTextViewEngineErrorCode;
+    private TextView mTextViewUpdateInfo;
+    private Button mButtonSwitchSlot;
+
+    private List<UpdateConfig> mConfigs;
+
+    private final UpdateManager mUpdateManager =
+            new UpdateManager(new UpdateEngine(), new PayloadSpecs());
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        setContentView(R.layout.activity_main);
+
+        this.mTextViewBuild = findViewById(R.id.textViewBuild);
+        this.mSpinnerConfigs = findViewById(R.id.spinnerConfigs);
+        this.mTextViewConfigsDirHint = findViewById(R.id.textViewConfigsDirHint);
+        this.mButtonReload = findViewById(R.id.buttonReload);
+        this.mButtonApplyConfig = findViewById(R.id.buttonApplyConfig);
+        this.mButtonStop = findViewById(R.id.buttonStop);
+        this.mButtonReset = findViewById(R.id.buttonReset);
+        this.mButtonSuspend = findViewById(R.id.buttonSuspend);
+        this.mButtonResume = findViewById(R.id.buttonResume);
+        this.mProgressBar = findViewById(R.id.progressBar);
+        this.mTextViewUpdaterState = findViewById(R.id.textViewUpdaterState);
+        this.mTextViewEngineStatus = findViewById(R.id.textViewEngineStatus);
+        this.mTextViewEngineErrorCode = findViewById(R.id.textViewEngineErrorCode);
+        this.mTextViewUpdateInfo = findViewById(R.id.textViewUpdateInfo);
+        this.mButtonSwitchSlot = findViewById(R.id.buttonSwitchSlot);
+
+        this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
+
+        uiResetWidgets();
+        loadUpdateConfigs();
+
+        this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange);
+        this.mUpdateManager.setOnEngineStatusUpdateCallback(this::onEngineStatusUpdate);
+        this.mUpdateManager.setOnEngineCompleteCallback(this::onEnginePayloadApplicationComplete);
+        this.mUpdateManager.setOnProgressUpdateCallback(this::onProgressUpdate);
+    }
+
+    @Override
+    protected void onDestroy() {
+        this.mUpdateManager.setOnEngineStatusUpdateCallback(null);
+        this.mUpdateManager.setOnProgressUpdateCallback(null);
+        this.mUpdateManager.setOnEngineCompleteCallback(null);
+        super.onDestroy();
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        // Binding to UpdateEngine invokes onStatusUpdate callback,
+        // persisted updater state has to be loaded and prepared beforehand.
+        this.mUpdateManager.bind();
+    }
+
+    @Override
+    protected void onPause() {
+        this.mUpdateManager.unbind();
+        super.onPause();
+    }
+
+    /**
+     * reload button is clicked
+     */
+    public void onReloadClick(View view) {
+        loadUpdateConfigs();
+    }
+
+    /**
+     * view config button is clicked
+     */
+    public void onViewConfigClick(View view) {
+        UpdateConfig config = mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+        new AlertDialog.Builder(this)
+                .setTitle(config.getName())
+                .setMessage(config.getRawJson())
+                .setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss())
+                .show();
+    }
+
+    /**
+     * apply config button is clicked
+     */
+    public void onApplyConfigClick(View view) {
+        new AlertDialog.Builder(this)
+                .setTitle("Apply Update")
+                .setMessage("Do you really want to apply this update?")
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+                    uiResetWidgets();
+                    uiResetEngineText();
+                    applyUpdate(getSelectedConfig());
+                })
+                .setNegativeButton(android.R.string.cancel, null)
+                .show();
+    }
+
+    private void applyUpdate(UpdateConfig config) {
+        try {
+            mUpdateManager.applyUpdate(this, config);
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to apply update " + config.getName(), e);
+        }
+    }
+
+    /**
+     * stop button clicked
+     */
+    public void onStopClick(View view) {
+        new AlertDialog.Builder(this)
+                .setTitle("Stop Update")
+                .setMessage("Do you really want to cancel running update?")
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+                    cancelRunningUpdate();
+                })
+                .setNegativeButton(android.R.string.cancel, null).show();
+    }
+
+    private void cancelRunningUpdate() {
+        try {
+            mUpdateManager.cancelRunningUpdate();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to cancel running update", e);
+        }
+    }
+
+    /**
+     * reset button clicked
+     */
+    public void onResetClick(View view) {
+        new AlertDialog.Builder(this)
+                .setTitle("Reset Update")
+                .setMessage("Do you really want to cancel running update"
+                        + " and restore old version?")
+                .setIcon(android.R.drawable.ic_dialog_alert)
+                .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+                    resetUpdate();
+                })
+                .setNegativeButton(android.R.string.cancel, null).show();
+    }
+
+    private void resetUpdate() {
+        try {
+            mUpdateManager.resetUpdate();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to reset update", e);
+        }
+    }
+
+    /**
+     * suspend button clicked
+     */
+    public void onSuspendClick(View view) {
+        try {
+            mUpdateManager.suspend();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to suspend running update", e);
+        }
+    }
+
+    /**
+     * resume button clicked
+     */
+    public void onResumeClick(View view) {
+        try {
+            uiResetWidgets();
+            uiResetEngineText();
+            mUpdateManager.resume();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to resume running update", e);
+        }
+    }
+
+    /**
+     * switch slot button clicked
+     */
+    public void onSwitchSlotClick(View view) {
+        uiResetWidgets();
+        mUpdateManager.setSwitchSlotOnReboot();
+    }
+
+    /**
+     * Invoked when SystemUpdaterSample app state changes.
+     * Value of {@code state} will be one of the
+     * values from {@link UpdaterState}.
+     */
+    private void onUpdaterStateChange(int state) {
+        Log.i(TAG, "UpdaterStateChange state="
+                + UpdaterState.getStateText(state)
+                + "/" + state);
+        runOnUiThread(() -> {
+            setUiUpdaterState(state);
+
+            if (state == UpdaterState.IDLE) {
+                uiStateIdle();
+            } else if (state == UpdaterState.RUNNING) {
+                uiStateRunning();
+            } else if (state == UpdaterState.PAUSED) {
+                uiStatePaused();
+            } else if (state == UpdaterState.ERROR) {
+                uiStateError();
+            } else if (state == UpdaterState.SLOT_SWITCH_REQUIRED) {
+                uiStateSlotSwitchRequired();
+            } else if (state == UpdaterState.REBOOT_REQUIRED) {
+                uiStateRebootRequired();
+            }
+        });
+    }
+
+    /**
+     * Invoked when {@link UpdateEngine} status changes. Value of {@code status} will
+     * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
+     */
+    private void onEngineStatusUpdate(int status) {
+        Log.i(TAG, "StatusUpdate - status="
+                + UpdateEngineStatuses.getStatusText(status)
+                + "/" + status);
+        runOnUiThread(() -> {
+            setUiEngineStatus(status);
+        });
+    }
+
+    /**
+     * Invoked when the payload has been applied, whether successfully or
+     * unsuccessfully. The value of {@code errorCode} will be one of the
+     * values from {@link UpdateEngine.ErrorCodeConstants}.
+     */
+    private void onEnginePayloadApplicationComplete(int errorCode) {
+        final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+                ? "SUCCESS"
+                : "FAILURE";
+        Log.i(TAG,
+                "PayloadApplicationCompleted - errorCode="
+                        + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+                        + " " + completionState);
+        runOnUiThread(() -> {
+            setUiEngineErrorCode(errorCode);
+        });
+    }
+
+    /**
+     * Invoked when update progress changes.
+     */
+    private void onProgressUpdate(double progress) {
+        mProgressBar.setProgress((int) (100 * progress));
+    }
+
+    /** resets ui */
+    private void uiResetWidgets() {
+        mTextViewBuild.setText(Build.DISPLAY);
+        mSpinnerConfigs.setEnabled(false);
+        mButtonReload.setEnabled(false);
+        mButtonApplyConfig.setEnabled(false);
+        mButtonStop.setEnabled(false);
+        mButtonReset.setEnabled(false);
+        mButtonSuspend.setEnabled(false);
+        mButtonResume.setEnabled(false);
+        mProgressBar.setEnabled(false);
+        mProgressBar.setVisibility(ProgressBar.INVISIBLE);
+        mButtonSwitchSlot.setEnabled(false);
+        mTextViewUpdateInfo.setTextColor(Color.parseColor("#aaaaaa"));
+    }
+
+    private void uiResetEngineText() {
+        mTextViewEngineStatus.setText(R.string.unknown);
+        mTextViewEngineErrorCode.setText(R.string.unknown);
+        // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies updater state properly.
+    }
+
+    private void uiStateIdle() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mSpinnerConfigs.setEnabled(true);
+        mButtonReload.setEnabled(true);
+        mButtonApplyConfig.setEnabled(true);
+        mProgressBar.setProgress(0);
+    }
+
+    private void uiStateRunning() {
+        uiResetWidgets();
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+        mButtonStop.setEnabled(true);
+        mButtonSuspend.setEnabled(true);
+    }
+
+    private void uiStatePaused() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+        mButtonResume.setEnabled(true);
+    }
+
+    private void uiStateSlotSwitchRequired() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+        mButtonSwitchSlot.setEnabled(true);
+        mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777"));
+    }
+
+    private void uiStateError() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+    }
+
+    private void uiStateRebootRequired() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+    }
+
+    /**
+     * loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
+     */
+    private void loadUpdateConfigs() {
+        mConfigs = UpdateConfigs.getUpdateConfigs(this);
+        loadConfigsToSpinner(mConfigs);
+    }
+
+    /**
+     * @param status update engine status code
+     */
+    private void setUiEngineStatus(int status) {
+        String statusText = UpdateEngineStatuses.getStatusText(status);
+        mTextViewEngineStatus.setText(statusText + "/" + status);
+    }
+
+    /**
+     * @param errorCode update engine error code
+     */
+    private void setUiEngineErrorCode(int errorCode) {
+        String errorText = UpdateEngineErrorCodes.getCodeName(errorCode);
+        mTextViewEngineErrorCode.setText(errorText + "/" + errorCode);
+    }
+
+    /**
+     * @param state updater sample state
+     */
+    private void setUiUpdaterState(int state) {
+        String stateText = UpdaterState.getStateText(state);
+        mTextViewUpdaterState.setText(stateText + "/" + state);
+    }
+
+    private void loadConfigsToSpinner(List<UpdateConfig> configs) {
+        String[] spinnerArray = UpdateConfigs.configsToNames(configs);
+        ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this,
+                android.R.layout.simple_spinner_item,
+                spinnerArray);
+        spinnerArrayAdapter.setDropDownViewResource(android.R.layout
+                .simple_spinner_dropdown_item);
+        mSpinnerConfigs.setAdapter(spinnerArrayAdapter);
+    }
+
+    private UpdateConfig getSelectedConfig() {
+        return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
new file mode 100644
index 0000000..ddd0919
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * Downloads chunk of a file from given url using {@code offset} and {@code size},
+ * and saves to a given location.
+ *
+ * In real-life application this helper class should download from HTTP Server,
+ * but in this sample app it will only download from a local file.
+ */
+public final class FileDownloader {
+
+    private String mUrl;
+    private long mOffset;
+    private long mSize;
+    private File mDestination;
+
+    public FileDownloader(String url, long offset, long size, File destination) {
+        this.mUrl = url;
+        this.mOffset = offset;
+        this.mSize = size;
+        this.mDestination = destination;
+    }
+
+    /**
+     * Downloads the file with given offset and size.
+     * @throws IOException when can't download the file
+     */
+    public void download() throws IOException {
+        Log.d("FileDownloader", "downloading " + mDestination.getName()
+                + " from " + mUrl
+                + " to " + mDestination.getAbsolutePath());
+
+        URL url = new URL(mUrl);
+        URLConnection connection = url.openConnection();
+        connection.connect();
+
+        // download the file
+        try (InputStream input = connection.getInputStream()) {
+            try (OutputStream output = new FileOutputStream(mDestination)) {
+                long skipped = input.skip(mOffset);
+                if (skipped != mOffset) {
+                    throw new IOException("Can't download file "
+                            + mUrl
+                            + " with given offset "
+                            + mOffset);
+                }
+                byte[] data = new byte[4096];
+                long total = 0;
+                while (total < mSize) {
+                    int needToRead = (int) Math.min(4096, mSize - total);
+                    int count = input.read(data, 0, needToRead);
+                    if (count <= 0) {
+                        break;
+                    }
+                    output.write(data, 0, count);
+                    total += count;
+                }
+                if (total != mSize) {
+                    throw new IOException("Can't download file "
+                            + mUrl
+                            + " with given size "
+                            + mSize);
+                }
+            }
+        }
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
new file mode 100644
index 0000000..b485234
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+/** Utility class for an OTA package. */
+public final class PackageFiles {
+
+    /**
+     * Directory used to perform updates.
+     */
+    public static final String OTA_PACKAGE_DIR = "/data/ota_package";
+
+    /**
+     * update payload, it will be passed to {@code UpdateEngine#applyPayload}.
+     */
+    public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
+
+    /**
+     * Currently, when calling {@code UpdateEngine#applyPayload} to perform actions
+     * that don't require network access (e.g. change slot), update_engine still
+     * talks to the server to download/verify file.
+     * {@code update_engine} might throw error when rebooting if {@code UpdateEngine#applyPayload}
+     * is not supplied right headers and tokens.
+     * This behavior might change in future android versions.
+     *
+     * To avoid extra network request in {@code update_engine}, this file has to be
+     * downloaded and put in {@code OTA_PACKAGE_DIR}.
+     */
+    public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
+
+    public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
+
+    /** The zip entry in an A/B OTA package, which will be used by update_verifier. */
+    public static final String CARE_MAP_FILE_NAME = "care_map.txt";
+
+    public static final String METADATA_FILE_NAME = "metadata";
+
+    /**
+     * The zip file that claims the compatibility of the update package to check against the Android
+     * framework to ensure that the package can be installed on the device.
+     */
+    public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
+
+    private PackageFiles() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
new file mode 100644
index 0000000..f062317
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** The helper class that creates {@link PayloadSpec}. */
+public class PayloadSpecs {
+
+    public PayloadSpecs() {}
+
+    /**
+     * The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package
+     * format. We want to find out the offset of the entry, so that we can pass it over to the A/B
+     * updater without making an extra copy of the payload.
+     *
+     * <p>According to Android docs, the entries are listed in the order in which they appear in the
+     * zip file. So we enumerate the entries to identify the offset of the payload file.
+     * http://developer.android.com/reference/java/util/zip/ZipFile.html#entries()
+     */
+    public PayloadSpec forNonStreaming(File packageFile) throws IOException {
+        boolean payloadFound = false;
+        long payloadOffset = 0;
+        long payloadSize = 0;
+
+        List<String> properties = new ArrayList<>();
+        try (ZipFile zip = new ZipFile(packageFile)) {
+            Enumeration<? extends ZipEntry> entries = zip.entries();
+            long offset = 0;
+            while (entries.hasMoreElements()) {
+                ZipEntry entry = entries.nextElement();
+                String name = entry.getName();
+                // Zip local file header has 30 bytes + filename + sizeof extra field.
+                // https://en.wikipedia.org/wiki/Zip_(file_format)
+                long extraSize = entry.getExtra() == null ? 0 : entry.getExtra().length;
+                offset += 30 + name.length() + extraSize;
+
+                if (entry.isDirectory()) {
+                    continue;
+                }
+
+                long length = entry.getCompressedSize();
+                if (PackageFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
+                    if (entry.getMethod() != ZipEntry.STORED) {
+                        throw new IOException("Invalid compression method.");
+                    }
+                    payloadFound = true;
+                    payloadOffset = offset;
+                    payloadSize = length;
+                } else if (PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
+                    InputStream inputStream = zip.getInputStream(entry);
+                    if (inputStream != null) {
+                        BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
+                        String line;
+                        while ((line = br.readLine()) != null) {
+                            properties.add(line);
+                        }
+                    }
+                }
+                offset += length;
+            }
+        }
+
+        if (!payloadFound) {
+            throw new IOException("Failed to find payload entry in the given package.");
+        }
+        return PayloadSpec.newBuilder()
+                        .url("file://" + packageFile.getAbsolutePath())
+                        .offset(payloadOffset)
+                        .size(payloadSize)
+                        .properties(properties)
+                        .build();
+    }
+
+    /**
+     * Creates a {@link PayloadSpec} for streaming update.
+     */
+    public PayloadSpec forStreaming(String updateUrl,
+                                           long offset,
+                                           long size,
+                                           File propertiesFile) throws IOException {
+        return PayloadSpec.newBuilder()
+                .url(updateUrl)
+                .offset(offset)
+                .size(size)
+                .properties(Files.readAllLines(propertiesFile.toPath()))
+                .build();
+    }
+
+    /**
+     * Converts an {@link PayloadSpec} to a string.
+     */
+    public String specToString(PayloadSpec payloadSpec) {
+        return "<PayloadSpec url=" + payloadSpec.getUrl()
+                + ", offset=" + payloadSpec.getOffset()
+                + ", size=" + payloadSpec.getSize()
+                + ", properties=" + Arrays.toString(
+                        payloadSpec.getProperties().toArray(new String[0]))
+                + ">";
+    }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
new file mode 100644
index 0000000..bbefcaf
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Utility class for working with json update configurations.
+ */
+public final class UpdateConfigs {
+
+    public static final String UPDATE_CONFIGS_ROOT = "configs/";
+
+    /**
+     * @param configs update configs
+     * @return list of names
+     */
+    public static String[] configsToNames(List<UpdateConfig> configs) {
+        return configs.stream().map(UpdateConfig::getName).toArray(String[]::new);
+    }
+
+    /**
+     * @param context app context
+     * @return configs root directory
+     */
+    public static String getConfigsRoot(Context context) {
+        return Paths
+                .get(context.getFilesDir().toString(), UPDATE_CONFIGS_ROOT)
+                .toString();
+    }
+
+    /**
+     * @param context application context
+     * @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
+     */
+    public static List<UpdateConfig> getUpdateConfigs(Context context) {
+        File root = new File(getConfigsRoot(context));
+        ArrayList<UpdateConfig> configs = new ArrayList<>();
+        if (!root.exists()) {
+            return configs;
+        }
+        for (final File f : root.listFiles()) {
+            if (!f.isDirectory() && f.getName().endsWith(".json")) {
+                try {
+                    String json = new String(Files.readAllBytes(f.toPath()),
+                            StandardCharsets.UTF_8);
+                    configs.add(UpdateConfig.fromJson(json));
+                } catch (Exception e) {
+                    Log.e("UpdateConfigs", "Can't read/parse config file " + f.getName(), e);
+                    throw new RuntimeException(
+                            "Can't read/parse config file " + f.getName(), e);
+                }
+            }
+        }
+        return configs;
+    }
+
+    /**
+     * @param filename searches by given filename
+     * @param config searches in {@link UpdateConfig#getAbConfig()}
+     * @return offset and size of {@code filename} in the package zip file
+     *         stored as {@link UpdateConfig.PackageFile}.
+     */
+    public static Optional<UpdateConfig.PackageFile> getPropertyFile(
+            final String filename,
+            UpdateConfig config) {
+        return Arrays
+                .stream(config.getAbConfig().getPropertyFiles())
+                .filter(file -> filename.equals(file.getFilename()))
+                .findFirst();
+    }
+
+    private UpdateConfigs() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
new file mode 100644
index 0000000..13df88b
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.os.UpdateEngine;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in  {@link UpdateEngine.ErrorCodeConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineErrorCodes {
+
+    /**
+    * Error code from the update engine. Values must agree with the ones in
+    * system/update_engine/common/error_code.h.
+    */
+    public static final int UNKNOWN = -1;
+    public static final int UPDATED_BUT_NOT_ACTIVE = 52;
+    public static final int USER_CANCELLED = 48;
+
+    private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
+
+    static {
+        CODE_TO_NAME_MAP.put(0, "SUCCESS");
+        CODE_TO_NAME_MAP.put(1, "ERROR");
+        CODE_TO_NAME_MAP.put(4, "FILESYSTEM_COPIER_ERROR");
+        CODE_TO_NAME_MAP.put(5, "POST_INSTALL_RUNNER_ERROR");
+        CODE_TO_NAME_MAP.put(6, "PAYLOAD_MISMATCHED_TYPE_ERROR");
+        CODE_TO_NAME_MAP.put(7, "INSTALL_DEVICE_OPEN_ERROR");
+        CODE_TO_NAME_MAP.put(8, "KERNEL_DEVICE_OPEN_ERROR");
+        CODE_TO_NAME_MAP.put(9, "DOWNLOAD_TRANSFER_ERROR");
+        CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
+        CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
+        CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
+        CODE_TO_NAME_MAP.put(15, "NEW_ROOTFS_VERIFICATION_ERROR");
+        CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
+        CODE_TO_NAME_MAP.put(26, "DOWNLOAD_METADATA_SIGNATURE_MISMATCH");
+        CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
+        CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
+    }
+
+    /**
+     * Completion codes returned by update engine indicating that the update
+     * was successfully applied.
+     */
+    private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<>(
+            Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
+                    // UPDATED_BUT_NOT_ACTIVE is returned when the payload is
+                    // successfully applied but the
+                    // device won't switch to the new slot after the next boot.
+                    UPDATED_BUT_NOT_ACTIVE));
+
+    /**
+     * checks if update succeeded using errorCode
+     */
+    public static boolean isUpdateSucceeded(int errorCode) {
+        return SUCCEEDED_COMPLETION_CODES.contains(errorCode);
+    }
+
+    /**
+     * converts error code to error name
+     */
+    public static String getCodeName(int errorCode) {
+        return CODE_TO_NAME_MAP.get(errorCode);
+    }
+
+    private UpdateEngineErrorCodes() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
new file mode 100644
index 0000000..e368f14
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+/**
+ * Utility class for properties that will be passed to {@code UpdateEngine#applyPayload}.
+ */
+public final class UpdateEngineProperties {
+
+    /**
+     * The property indicating that the update engine should not switch slot
+     * when the device reboots.
+     */
+    public static final String PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT = "SWITCH_SLOT_ON_REBOOT=0";
+
+    /**
+     * The property to skip post-installation.
+     * https://source.android.com/devices/tech/ota/ab/#post-installation
+     */
+    public static final String PROPERTY_SKIP_POST_INSTALL = "RUN_POST_INSTALL=0";
+
+    private UpdateEngineProperties() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
new file mode 100644
index 0000000..a96f19d
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.util.SparseArray;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in  {@code UpdateEngine.UpdateStatusConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineStatuses {
+
+    private static final SparseArray<String> STATUS_MAP = new SparseArray<>();
+
+    static {
+        STATUS_MAP.put(0, "IDLE");
+        STATUS_MAP.put(1, "CHECKING_FOR_UPDATE");
+        STATUS_MAP.put(2, "UPDATE_AVAILABLE");
+        STATUS_MAP.put(3, "DOWNLOADING");
+        STATUS_MAP.put(4, "VERIFYING");
+        STATUS_MAP.put(5, "FINALIZING");
+        STATUS_MAP.put(6, "UPDATED_NEED_REBOOT");
+        STATUS_MAP.put(7, "REPORTING_ERROR_EVENT");
+        STATUS_MAP.put(8, "ATTEMPTING_ROLLBACK");
+        STATUS_MAP.put(9, "DISABLED");
+    }
+
+    /**
+     * converts status code to status name
+     */
+    public static String getStatusText(int status) {
+        return STATUS_MAP.get(status);
+    }
+
+    private UpdateEngineStatuses() {}
+}
diff --git a/updater_sample/tests/Android.bp b/updater_sample/tests/Android.bp
new file mode 100644
index 0000000..c2783ef
--- /dev/null
+++ b/updater_sample/tests/Android.bp
@@ -0,0 +1,40 @@
+// 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.
+
+android_test {
+    name: "SystemUpdaterSampleTests",
+    sdk_version: "system_current",
+
+    libs: [
+        "android.test.base.stubs",
+        "android.test.runner.stubs",
+        "SystemUpdaterSample",
+    ],
+
+    static_libs: [
+        "android-support-test",
+        "mockito-target-minus-junit4",
+        "guava",
+    ],
+
+    instrumentation_for: "com.example.android.systemupdatersample",
+
+    optimize: {
+        enabled: false,
+    },
+
+    resource_dirs: ["res"],
+
+    srcs: ["src/**/*.java"],
+}
diff --git a/updater_sample/tests/AndroidManifest.xml b/updater_sample/tests/AndroidManifest.xml
new file mode 100644
index 0000000..76af5f1
--- /dev/null
+++ b/updater_sample/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+      package="com.example.android.systemupdatersample.tests">
+
+    <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
+
+    <!-- We add an application tag here just so that we can indicate that
+         this package needs to link against the android.test library,
+         which is needed when building test cases. -->
+    <application>
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+                     android:targetPackage="com.example.android.systemupdatersample"
+                     android:label="Tests for SystemUpdaterSample."/>
+
+</manifest>
diff --git a/updater_sample/tests/build.properties b/updater_sample/tests/build.properties
new file mode 100644
index 0000000..e0c39de
--- /dev/null
+++ b/updater_sample/tests/build.properties
@@ -0,0 +1 @@
+tested.project.dir=..
diff --git a/updater_sample/tests/res/raw/ota_002_package.zip b/updater_sample/tests/res/raw/ota_002_package.zip
new file mode 100644
index 0000000..6bf2a23
--- /dev/null
+++ b/updater_sample/tests/res/raw/ota_002_package.zip
Binary files differ
diff --git a/updater_sample/tests/res/raw/update_config_001_stream.json b/updater_sample/tests/res/raw/update_config_001_stream.json
new file mode 100644
index 0000000..b024ad9
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_001_stream.json
@@ -0,0 +1,16 @@
+{
+    "name": "streaming-001",
+    "url": "http://foo.bar/update.zip",
+    "ab_install_type": "STREAMING",
+    "ab_config": {
+        "verify_payload_metadata": true,
+        "force_switch_slot": true,
+        "property_files": [
+            {
+                "filename": "payload.bin",
+                "offset": 195,
+                "size": 8
+            }
+        ]
+    }
+}
diff --git a/updater_sample/tests/res/raw/update_config_002_stream.json b/updater_sample/tests/res/raw/update_config_002_stream.json
new file mode 100644
index 0000000..12c18bb
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_002_stream.json
@@ -0,0 +1,42 @@
+{
+    "__": "*** Generated using tools/gen_update_config.py ***",
+    "ab_config": {
+        "verify_payload_metadata": true,
+        "force_switch_slot": false,
+        "property_files": [
+            {
+                "filename": "payload_metadata.bin",
+                "offset": 41,
+                "size": 827
+            },
+            {
+                "filename": "payload.bin",
+                "offset": 41,
+                "size": 1392
+            },
+            {
+                "filename": "payload_properties.txt",
+                "offset": 1485,
+                "size": 147
+            },
+            {
+                "filename": "care_map.txt",
+                "offset": 1674,
+                "size": 12
+            },
+            {
+                "filename": "compatibility.zip",
+                "offset": 1733,
+                "size": 17
+            },
+            {
+                "filename": "metadata",
+                "offset": 1809,
+                "size": 29
+            }
+        ]
+    },
+    "ab_install_type": "STREAMING",
+    "name": "S ota_002_package",
+    "url": "file:///data/my-sample-ota-builds-dir/ota_002_package.zip"
+}
\ No newline at end of file
diff --git a/updater_sample/tests/res/raw/update_config_003_nonstream.json b/updater_sample/tests/res/raw/update_config_003_nonstream.json
new file mode 100644
index 0000000..2011f76
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_003_nonstream.json
@@ -0,0 +1,17 @@
+{
+    "__": "*** Generated using tools/gen_update_config.py ***",
+    "ab_config": {
+        "verify_payload_metadata": true,
+        "force_switch_slot": false,
+        "property_files": [
+            {
+                "filename": "payload.bin",
+                "offset": 195,
+                "size": 8
+            }
+        ]
+    },
+    "ab_install_type": "NON_STREAMING",
+    "name": "S ota_002_package",
+    "url": "file:///data/my-sample-ota-builds-dir/ota_003_package.zip"
+}
\ No newline at end of file
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
new file mode 100644
index 0000000..48d0e42
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+import com.google.common.io.CharStreams;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Tests for {@link UpdateConfig}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigTest {
+
+    private static final String JSON_NON_STREAMING = "{"
+            + " \"name\": \"vip update\", \"url\": \"file:///my-builds/a.zip\","
+            + " \"ab_install_type\": \"NON_STREAMING\","
+            + " \"ab_config\": {"
+            + "     \"force_switch_slot\": false,"
+            + "     \"verify_payload_metadata\": false } }";
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    private Context mContext;
+    private Context mTargetContext;
+    private String mJsonStreaming001;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        mJsonStreaming001 = readResource(R.raw.update_config_001_stream);
+    }
+
+    @Test
+    public void fromJson_parsesNonStreaming() throws Exception {
+        UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
+        assertEquals("name is parsed", "vip update", config.getName());
+        assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
+        assertSame("type is parsed",
+                UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
+                config.getInstallType());
+        assertEquals("url is parsed", "file:///my-builds/a.zip", config.getUrl());
+    }
+
+    @Test
+    public void fromJson_parsesStreaming() throws Exception {
+        UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
+        assertEquals("streaming-001", config.getName());
+        assertEquals("http://foo.bar/update.zip", config.getUrl());
+        assertSame(UpdateConfig.AB_INSTALL_TYPE_STREAMING, config.getInstallType());
+        assertEquals("payload.bin",
+                config.getAbConfig().getPropertyFiles()[0].getFilename());
+        assertEquals(195, config.getAbConfig().getPropertyFiles()[0].getOffset());
+        assertEquals(8, config.getAbConfig().getPropertyFiles()[0].getSize());
+        assertTrue(config.getAbConfig().getForceSwitchSlot());
+    }
+
+    @Test
+    public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
+        UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
+        thrown.expect(RuntimeException.class);
+        config.getUpdatePackageFile();
+    }
+
+    @Test
+    public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
+        String json = "{"
+                + " \"name\": \"upd\", \"url\": \"http://foo.bar\","
+                + " \"ab_install_type\": \"NON_STREAMING\","
+                + " \"ab_config\": {"
+                + "     \"force_switch_slot\": false,"
+                + "     \"verify_payload_metadata\": false } }";
+        UpdateConfig config = UpdateConfig.fromJson(json);
+        thrown.expect(RuntimeException.class);
+        config.getUpdatePackageFile();
+    }
+
+    @Test
+    public void getUpdatePackageFile_works() throws Exception {
+        UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
+        assertEquals("/my-builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
+    }
+
+    private String readResource(int id) throws IOException {
+        return CharStreams.toString(new InputStreamReader(
+            mContext.getResources().openRawResource(id)));
+    }
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
new file mode 100644
index 0000000..e05ad29
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CharStreams;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Tests for {@link UpdateManager}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateManagerTest {
+
+    @Rule
+    public MockitoRule mockito = MockitoJUnit.rule();
+
+    @Mock
+    private UpdateEngine mUpdateEngine;
+    @Mock
+    private PayloadSpecs mPayloadSpecs;
+    private UpdateManager mSubject;
+    private Context mContext;
+    private UpdateConfig mNonStreamingUpdate003;
+
+    @Before
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs);
+        mNonStreamingUpdate003 =
+                UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream));
+    }
+
+    @Test
+    public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception {
+        PayloadSpec payload = buildMockPayloadSpec();
+        when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
+        when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
+            // When UpdateManager is bound to update_engine, it passes
+            // UpdateEngineCallback as a callback to update_engine.
+            UpdateEngineCallback callback = answer.getArgument(0);
+            callback.onStatusUpdate(
+                    UpdateEngine.UpdateStatusConstants.IDLE,
+                    /*engineProgress*/ 0.0f);
+            return null;
+        });
+
+        mSubject.bind();
+        mSubject.applyUpdate(null, mNonStreamingUpdate003);
+
+        verify(mUpdateEngine).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[] {
+                        "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
+                });
+    }
+
+    @Test
+    public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Exception {
+        PayloadSpec payload = buildMockPayloadSpec();
+        when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
+        when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
+            // When UpdateManager is bound to update_engine, it passes
+            // UpdateEngineCallback as a callback to update_engine.
+            UpdateEngineCallback callback = answer.getArgument(0);
+            callback.onStatusUpdate(
+                    UpdateEngine.UpdateStatusConstants.IDLE,
+                    /*engineProgress*/ 0.0f);
+            return null;
+        });
+
+        mSubject.bind();
+        mSubject.applyUpdate(null, mNonStreamingUpdate003);
+        mSubject.unbind();
+        mSubject.bind(); // re-bind - now it should re-apply last update
+
+        assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING);
+        // it should be called 2 times
+        verify(mUpdateEngine, times(2)).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[] {
+                        "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
+                });
+    }
+
+    private PayloadSpec buildMockPayloadSpec() {
+        PayloadSpec payload = mock(PayloadSpec.class);
+        when(payload.getUrl()).thenReturn("file://blah");
+        when(payload.getOffset()).thenReturn(120L);
+        when(payload.getSize()).thenReturn(340L);
+        when(payload.getProperties()).thenReturn(ImmutableList.of());
+        return payload;
+    }
+
+    private String readResource(int id) throws IOException {
+        return CharStreams.toString(new InputStreamReader(
+                mContext.getResources().openRawResource(id)));
+    }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
new file mode 100644
index 0000000..a136ff0
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Tests for {@link FileDownloader}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FileDownloaderTest {
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    private Context mTestContext;
+    private Context mTargetContext;
+
+    @Before
+    public void setUp() {
+        mTestContext = InstrumentationRegistry.getContext();
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+    }
+
+    @Test
+    public void download_downloadsChunkOfZip() throws Exception {
+        // Prepare the target file
+        File packageFile = Paths
+                .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
+                .toFile();
+        Files.deleteIfExists(packageFile.toPath());
+        Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
+                packageFile.toPath());
+        String url = "file://" + packageFile.getAbsolutePath();
+        // prepare where to download
+        File outFile = Paths
+                .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt")
+                .toFile();
+        Files.deleteIfExists(outFile.toPath());
+        // download a chunk of ota.zip
+        FileDownloader downloader = new FileDownloader(url, 1674, 12, outFile);
+        downloader.download();
+        String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath()));
+        // archive contains text files with uppercase filenames
+        assertEquals("CARE_MAP-TXT", downloadedContent);
+    }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
new file mode 100644
index 0000000..0308693
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.tests.R;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+
+/**
+ * Tests if PayloadSpecs parses update package zip file correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PayloadSpecsTest {
+
+    private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
+
+    private File mTestDir;
+
+    private Context mTargetContext;
+    private Context mTestContext;
+
+    private PayloadSpecs mPayloadSpecs;
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    @Before
+    public void setUp() {
+        mTargetContext = InstrumentationRegistry.getTargetContext();
+        mTestContext = InstrumentationRegistry.getContext();
+
+        mTestDir = mTargetContext.getCacheDir();
+        mPayloadSpecs = new PayloadSpecs();
+    }
+
+    @Test
+    public void forNonStreaming_works() throws Exception {
+        // Prepare the target file
+        File packageFile = Paths
+                .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
+                .toFile();
+        java.nio.file.Files.deleteIfExists(packageFile.toPath());
+        java.nio.file.Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
+                packageFile.toPath());
+        PayloadSpec spec = mPayloadSpecs.forNonStreaming(packageFile);
+
+        assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
+        assertEquals("correct payload offset",
+                30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
+        assertEquals("correct payload size", 1392, spec.getSize());
+        assertEquals(4, spec.getProperties().size());
+        assertEquals(
+                "FILE_HASH=sEAK/NMbU7GGe01xt55FsPafIPk8IYyBOAd6SiDpiMs=",
+                spec.getProperties().get(0));
+    }
+
+    @Test
+    public void forNonStreaming_IOException() throws Exception {
+        thrown.expect(IOException.class);
+        mPayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
+    }
+
+    @Test
+    public void forStreaming_works() throws Exception {
+        String url = "http://a.com/b.zip";
+        long offset = 45;
+        long size = 200;
+        File propertiesFile = createMockPropertiesFile();
+
+        PayloadSpec spec = mPayloadSpecs.forStreaming(url, offset, size, propertiesFile);
+        assertEquals("same url", url, spec.getUrl());
+        assertEquals("same offset", offset, spec.getOffset());
+        assertEquals("same size", size, spec.getSize());
+        assertArrayEquals("correct properties",
+                new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
+    }
+
+    private File createMockPropertiesFile() throws IOException {
+        File propertiesFile = new File(mTestDir, PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME);
+        Files.asCharSink(propertiesFile, Charsets.UTF_8).write(PROPERTIES_CONTENTS);
+        return propertiesFile;
+    }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
new file mode 100644
index 0000000..4ccae93
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link UpdateConfigs}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigsTest {
+
+    @Rule
+    public final ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void configsToNames_extractsNames() {
+        List<UpdateConfig> configs = Arrays.asList(
+                new UpdateConfig("blah", "http://", UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING),
+                new UpdateConfig("blah 2", "http://", UpdateConfig.AB_INSTALL_TYPE_STREAMING)
+        );
+        String[] names = UpdateConfigs.configsToNames(configs);
+        assertArrayEquals(new String[] {"blah", "blah 2"}, names);
+    }
+}
diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py
new file mode 100755
index 0000000..f2cb1a8
--- /dev/null
+++ b/updater_sample/tools/gen_update_config.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""
+Given a OTA package file, produces update config JSON file.
+
+Example:
+      $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+            bootable/recovery/updater_sample/tools/gen_update_config.py \\
+                --ab_install_type=STREAMING \\
+                ota-build-001.zip  \\
+                my-config-001.json \\
+                http://foo.bar/ota-builds/ota-build-001.zip
+"""
+
+import argparse
+import json
+import os.path
+import sys
+import zipfile
+
+import ota_from_target_files  # pylint: disable=import-error
+
+
+class GenUpdateConfig(object):
+    """
+    A class that generates update configuration file from an OTA package.
+
+    Currently supports only A/B (seamless) OTA packages.
+    TODO: add non-A/B packages support.
+    """
+
+    AB_INSTALL_TYPE_STREAMING = 'STREAMING'
+    AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
+
+    def __init__(self,
+                 package,
+                 url,
+                 ab_install_type,
+                 ab_force_switch_slot,
+                 ab_verify_payload_metadata):
+        self.package = package
+        self.url = url
+        self.ab_install_type = ab_install_type
+        self.ab_force_switch_slot = ab_force_switch_slot
+        self.ab_verify_payload_metadata = ab_verify_payload_metadata
+        self.streaming_required = (
+            # payload.bin and payload_properties.txt must exist.
+            'payload.bin',
+            'payload_properties.txt',
+        )
+        self.streaming_optional = (
+            # care_map.txt is available only if dm-verity is enabled.
+            'care_map.txt',
+            # compatibility.zip is available only if target supports Treble.
+            'compatibility.zip',
+        )
+        self._config = None
+
+    @property
+    def config(self):
+        """Returns generated config object."""
+        return self._config
+
+    def run(self):
+        """Generates config."""
+        self._config = {
+            '__': '*** Generated using tools/gen_update_config.py ***',
+            'name': self.ab_install_type[0] + ' ' + os.path.basename(self.package)[:-4],
+            'url': self.url,
+            'ab_config': self._gen_ab_config(),
+            'ab_install_type': self.ab_install_type,
+        }
+
+    def _gen_ab_config(self):
+        """Builds config required for A/B update."""
+        with zipfile.ZipFile(self.package, 'r') as package_zip:
+            config = {
+                'property_files': self._get_property_files(package_zip),
+                'verify_payload_metadata': self.ab_verify_payload_metadata,
+                'force_switch_slot': self.ab_force_switch_slot,
+            }
+
+        return config
+
+    @staticmethod
+    def _get_property_files(package_zip):
+        """Constructs the property-files list for A/B streaming metadata."""
+
+        ab_ota = ota_from_target_files.AbOtaPropertyFiles()
+        property_str = ab_ota.GetPropertyFilesString(package_zip, False)
+        property_files = []
+        for file in property_str.split(','):
+            filename, offset, size = file.split(':')
+            inner_file = {
+                'filename': filename,
+                'offset': int(offset),
+                'size': int(size)
+            }
+            property_files.append(inner_file)
+
+        return property_files
+
+    def write(self, out):
+        """Writes config to the output file."""
+        with open(out, 'w') as out_file:
+            json.dump(self.config, out_file, indent=4, separators=(',', ': '), sort_keys=True)
+
+
+def main():  # pylint: disable=missing-docstring
+    ab_install_type_choices = [
+        GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
+        GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING]
+    parser = argparse.ArgumentParser(description=__doc__,
+                                     formatter_class=argparse.RawDescriptionHelpFormatter)
+    parser.add_argument('--ab_install_type',
+                        type=str,
+                        default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
+                        choices=ab_install_type_choices,
+                        help='A/B update installation type')
+    parser.add_argument('--ab_force_switch_slot',
+                        default=False,
+                        action='store_true',
+                        help='if set device will boot to a new slot, otherwise user '
+                              'manually switches slot on the screen')
+    parser.add_argument('--ab_verify_payload_metadata',
+                        default=False,
+                        action='store_true',
+                        help='if set the app will verify the update payload metadata using '
+                             'update_engine before downloading the whole package.')
+    parser.add_argument('package',
+                        type=str,
+                        help='OTA package zip file')
+    parser.add_argument('out',
+                        type=str,
+                        help='Update configuration JSON file')
+    parser.add_argument('url',
+                        type=str,
+                        help='OTA package download url')
+    args = parser.parse_args()
+
+    if not args.out.endswith('.json'):
+        print('out must be a json file')
+        sys.exit(1)
+
+    gen = GenUpdateConfig(
+        package=args.package,
+        url=args.url,
+        ab_install_type=args.ab_install_type,
+        ab_force_switch_slot=args.ab_force_switch_slot,
+        ab_verify_payload_metadata=args.ab_verify_payload_metadata)
+    gen.run()
+    gen.write(args.out)
+    print('Config is written to ' + args.out)
+
+
+if __name__ == '__main__':
+    main()
diff --git a/updater_sample/tools/test_gen_update_config.py b/updater_sample/tools/test_gen_update_config.py
new file mode 100755
index 0000000..8b77cb2
--- /dev/null
+++ b/updater_sample/tools/test_gen_update_config.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# 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.
+
+"""
+Tests gen_update_config.py.
+
+Example:
+    $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+        python3 -m unittest test_gen_update_config
+"""
+
+import os.path
+import unittest
+from gen_update_config import GenUpdateConfig
+
+
+class GenUpdateConfigTest(unittest.TestCase): # pylint: disable=missing-docstring
+
+    def test_ab_install_type_streaming(self):
+        """tests if streaming property files' offset and size are generated properly"""
+        config, package = self._generate_config()
+        property_files = config['ab_config']['property_files']
+        self.assertEqual(len(property_files), 6)
+        with open(package, 'rb') as pkg_file:
+            for prop in property_files:
+                filename, offset, size = prop['filename'], prop['offset'], prop['size']
+                pkg_file.seek(offset)
+                raw_data = pkg_file.read(size)
+                if filename in ['payload.bin', 'payload_metadata.bin']:
+                    pass
+                elif filename == 'payload_properties.txt':
+                    pass
+                elif filename == 'metadata':
+                    self.assertEqual(raw_data.decode('ascii'), 'META-INF/COM/ANDROID/METADATA')
+                else:
+                    expected_data = filename.replace('.', '-').upper()
+                    self.assertEqual(raw_data.decode('ascii'), expected_data)
+
+    @staticmethod
+    def _generate_config():
+        """Generates JSON config from ota_002_package.zip."""
+        ota_package = os.path.join(os.path.dirname(__file__),
+                                   '../tests/res/raw/ota_002_package.zip')
+        gen = GenUpdateConfig(ota_package,
+                              'file:///foo.bar',
+                              GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
+                              True,  # ab_force_switch_slot
+                              True)  # ab_verify_payload_metadata
+        gen.run()
+        return gen.config, ota_package
diff --git a/vr_ui.cpp b/vr_ui.cpp
index a58c99e..a131a27 100644
--- a/vr_ui.cpp
+++ b/vr_ui.cpp
@@ -16,9 +16,15 @@
 
 #include "vr_ui.h"
 
-#include <minui/minui.h>
+#include <android-base/properties.h>
 
-VrRecoveryUI::VrRecoveryUI() : kStereoOffset(RECOVERY_UI_VR_STEREO_OFFSET) {}
+#include "minui/minui.h"
+
+constexpr int kDefaultStereoOffset = 0;
+
+VrRecoveryUI::VrRecoveryUI()
+    : stereo_offset_(
+          android::base::GetIntProperty("ro.recovery.ui.stereo_offset", kDefaultStereoOffset)) {}
 
 int VrRecoveryUI::ScreenWidth() const {
   return gr_fb_width() / 2;
@@ -30,36 +36,37 @@
 
 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);
+  gr_blit(surface, sx, sy, w, h, dx + stereo_offset_, dy);
+  gr_blit(surface, sx, sy, w, h, dx - stereo_offset_ + 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);
+  gr_texticon(x + stereo_offset_, y, surface);
+  gr_texticon(x - stereo_offset_ + ScreenWidth(), y, surface);
 }
 
-int VrRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
-  gr_text(gr_sys_font(), x + kStereoOffset, y, line, bold);
-  gr_text(gr_sys_font(), x - kStereoOffset + ScreenWidth(), y, line, bold);
+int VrRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
+  gr_text(gr_sys_font(), x + stereo_offset_, y, line.c_str(), bold);
+  gr_text(gr_sys_font(), x - stereo_offset_ + ScreenWidth(), y, line.c_str(), 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);
+  gr_fill(margin_width_ + stereo_offset_, y, ScreenWidth() - margin_width_ + stereo_offset_, y + 2);
+  gr_fill(ScreenWidth() + margin_width_ - stereo_offset_, y,
+          gr_fb_width() - margin_width_ - stereo_offset_, 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);
+  gr_fill(margin_width_ + stereo_offset_, y, ScreenWidth() - margin_width_ + stereo_offset_,
+          y + height);
+  gr_fill(ScreenWidth() + margin_width_ - stereo_offset_, y,
+          gr_fb_width() - margin_width_ - stereo_offset_, 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);
+  gr_fill(x + stereo_offset_, y, w, h);
+  gr_fill(x - stereo_offset_ + ScreenWidth(), y, w, h);
 }
diff --git a/vr_ui.h b/vr_ui.h
index eeb4589..63c0f24 100644
--- a/vr_ui.h
+++ b/vr_ui.h
@@ -17,6 +17,8 @@
 #ifndef RECOVERY_VR_UI_H
 #define RECOVERY_VR_UI_H
 
+#include <string>
+
 #include "screen_ui.h"
 
 class VrRecoveryUI : public ScreenRecoveryUI {
@@ -26,7 +28,7 @@
  protected:
   // Pixel offsets to move drawing functions to visible range.
   // Can vary per device depending on screen size and lens distortion.
-  const int kStereoOffset;
+  const int stereo_offset_;
 
   int ScreenWidth() const override;
   int ScreenHeight() const override;
@@ -36,7 +38,7 @@
   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;
+  int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
 };
 
 #endif  // RECOVERY_VR_UI_H
diff --git a/wear_ui.cpp b/wear_ui.cpp
index ca6b1b1..3b057b7 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -16,31 +16,31 @@
 
 #include "wear_ui.h"
 
-#include <pthread.h>
-#include <stdio.h>  // TODO: Remove after killing the call to sprintf().
 #include <string.h>
 
 #include <string>
+#include <vector>
 
 #include <android-base/properties.h>
 #include <android-base/strings.h>
 #include <minui/minui.h>
 
-WearRecoveryUI::WearRecoveryUI()
-    : kProgressBarBaseline(RECOVERY_UI_PROGRESS_BAR_BASELINE),
-      kMenuUnusableRows(RECOVERY_UI_MENU_UNUSABLE_ROWS) {
-  // TODO: kMenuUnusableRows should be computed based on the lines in draw_screen_locked().
+constexpr int kDefaultProgressBarBaseline = 259;
+constexpr int kDefaultMenuUnusableRows = 9;
 
-  // TODO: The following three variables are likely not needed. The first two are detected
-  // automatically in ScreenRecoveryUI::LoadAnimation(), based on the actual files seen on device.
-  intro_frames = 22;
-  loop_frames = 60;
+WearRecoveryUI::WearRecoveryUI()
+    : ScreenRecoveryUI(true),
+      progress_bar_baseline_(android::base::GetIntProperty("ro.recovery.ui.progress_bar_baseline",
+                                                           kDefaultProgressBarBaseline)),
+      menu_unusable_rows_(android::base::GetIntProperty("ro.recovery.ui.menu_unusable_rows",
+                                                        kDefaultMenuUnusableRows)) {
+  // TODO: menu_unusable_rows_ should be computed based on the lines in draw_screen_locked().
 
   touch_screen_allowed_ = true;
 }
 
 int WearRecoveryUI::GetProgressBaseline() const {
-  return kProgressBarBaseline;
+  return progress_bar_baseline_;
 }
 
 // Draw background frame on the screen.  Does not flip pages.
@@ -58,20 +58,17 @@
     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);
+
+    // Draw recovery text on screen above progress bar.
+    GRSurface* text = GetCurrentText();
+    int text_x = (ScreenWidth() - gr_get_width(text)) / 2;
+    int text_y = GetProgressBaseline() - gr_get_height(text) - 10;
+    gr_color(255, 255, 255, 255);
+    gr_texticon(text_x, text_y, text);
   }
 }
 
-static const char* SWIPE_HELP[] = {
-  "Swipe up/down to move.",
-  "Swipe left/right to select.",
-  "",
-  NULL
-};
-
-// TODO merge drawing routines with screen_ui
 void WearRecoveryUI::draw_screen_locked() {
-  char cur_selection_str[50];
-
   draw_background_locked();
   if (!show_text) {
     draw_foreground_locked();
@@ -79,68 +76,14 @@
     SetColor(TEXT_FILL);
     gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
-    int y = kMarginHeight;
-    int x = kMarginWidth;
-    if (show_menu) {
-      std::string recovery_fingerprint =
-          android::base::GetProperty("ro.bootimage.build.fingerprint", "");
-      SetColor(HEADER);
-      y += DrawTextLine(x + 4, y, "Android Recovery", true);
-      for (auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
-        y += DrawTextLine(x + 4, y, chunk.c_str(), false);
-      }
-
-      // This is actually the help strings.
-      y += DrawTextLines(x + 4, y, SWIPE_HELP);
-      SetColor(HEADER);
-      y += DrawTextLines(x + 4, y, menu_headers_);
-
-      // Show the current menu item number in relation to total number if
-      // items don't fit on the screen.
-      if (menu_items > menu_end - menu_start) {
-        sprintf(cur_selection_str, "Current item: %d/%d", menu_sel + 1, menu_items);
-        gr_text(gr_sys_font(), x + 4, y, cur_selection_str, 1);
-        y += char_height_ + 4;
-      }
-
-      // Menu begins here
-      SetColor(MENU);
-
-      for (int i = menu_start; i < menu_end; ++i) {
-        if (i == menu_sel) {
-          // draw the highlight bar
-          SetColor(MENU_SEL_BG);
-          gr_fill(x, y - 2, gr_fb_width() - x, y + char_height_ + 2);
-          // white text of selected item
-          SetColor(MENU_SEL_FG);
-          if (menu_[i][0]) {
-            gr_text(gr_sys_font(), x + 4, y, menu_[i].c_str(), 1);
-          }
-          SetColor(MENU);
-        } else if (menu_[i][0]) {
-          gr_text(gr_sys_font(), x + 4, y, menu_[i].c_str(), 0);
-        }
-        y += char_height_ + 4;
-      }
-      SetColor(MENU);
-      y += 4;
-      gr_fill(0, y, gr_fb_width(), y + 2);
-      y += 4;
-    }
-
-    SetColor(LOG);
-
-    // display from the bottom up, until we hit the top of the
-    // screen, the bottom of the menu, or we've displayed the
-    // entire text buffer.
-    int 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) {
-      gr_text(gr_sys_font(), x + 4, ty, text_[row], 0);
-      --row;
-      if (row < 0) row = text_rows_ - 1;
-    }
+    // clang-format off
+    static std::vector<std::string> SWIPE_HELP = {
+      "Swipe up/down to move.",
+      "Swipe left/right to select.",
+      "",
+    };
+    // clang-format on
+    draw_menu_and_text_buffer_locked(SWIPE_HELP);
   }
 }
 
@@ -152,49 +95,12 @@
 
 void WearRecoveryUI::SetStage(int /* current */, int /* max */) {}
 
-void WearRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
-                               int initial_selection) {
-  pthread_mutex_lock(&updateMutex);
+void WearRecoveryUI::StartMenu(const std::vector<std::string>& headers,
+                               const std::vector<std::string>& items, size_t initial_selection) {
+  std::lock_guard<std::mutex> lg(updateMutex);
   if (text_rows_ > 0 && text_cols_ > 0) {
-    menu_headers_ = headers;
-    menu_.clear();
-    // "i < text_rows_" is removed from the loop termination condition,
-    // which is different from the one in ScreenRecoveryUI::StartMenu().
-    // Because WearRecoveryUI supports scrollable menu, it's fine to have
-    // more entries than text_rows_. The menu may be truncated otherwise.
-    // Bug: 23752519
-    for (size_t i = 0; items[i] != nullptr; i++) {
-      menu_.emplace_back(std::string(items[i], strnlen(items[i], text_cols_ - 1)));
-    }
-    menu_items = static_cast<int>(menu_.size());
-    show_menu = true;
-    menu_sel = initial_selection;
-    menu_start = 0;
-    menu_end = text_rows_ - 1 - kMenuUnusableRows;
-    if (menu_items <= menu_end) menu_end = menu_items;
+    menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1,
+                                   text_cols_ - 1, headers, items, initial_selection);
     update_screen_locked();
   }
-  pthread_mutex_unlock(&updateMutex);
-}
-
-int WearRecoveryUI::SelectMenu(int sel) {
-  int old_sel;
-  pthread_mutex_lock(&updateMutex);
-  if (show_menu) {
-    old_sel = menu_sel;
-    menu_sel = sel;
-    if (menu_sel < 0) menu_sel = 0;
-    if (menu_sel >= menu_items) menu_sel = menu_items - 1;
-    if (menu_sel < menu_start) {
-      menu_start--;
-      menu_end--;
-    } else if (menu_sel >= menu_end && menu_sel < menu_items) {
-      menu_end++;
-      menu_start++;
-    }
-    sel = menu_sel;
-    if (menu_sel != old_sel) update_screen_locked();
-  }
-  pthread_mutex_unlock(&updateMutex);
-  return sel;
 }
diff --git a/wear_ui.h b/wear_ui.h
index 739b4cb..b80cfd7 100644
--- a/wear_ui.h
+++ b/wear_ui.h
@@ -17,6 +17,9 @@
 #ifndef RECOVERY_WEAR_UI_H
 #define RECOVERY_WEAR_UI_H
 
+#include <string>
+#include <vector>
+
 #include "screen_ui.h"
 
 class WearRecoveryUI : public ScreenRecoveryUI {
@@ -25,18 +28,16 @@
 
   void SetStage(int current, int max) override;
 
-  // menu display
-  void StartMenu(const char* const* headers, const char* const* items,
-                 int initial_selection) override;
-  int SelectMenu(int sel) override;
-
  protected:
   // progress bar vertical position, it's centered horizontally
-  const int kProgressBarBaseline;
+  const int progress_bar_baseline_;
 
   // Unusable rows when displaying the recovery menu, including the lines for headers (Android
   // Recovery, build id and etc) and the bottom lines that may otherwise go out of the screen.
-  const int kMenuUnusableRows;
+  const int menu_unusable_rows_;
+
+  void StartMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
+                 size_t initial_selection) override;
 
   int GetProgressBaseline() const override;
 
@@ -45,8 +46,6 @@
  private:
   void draw_background_locked() override;
   void draw_screen_locked() override;
-
-  int menu_start, menu_end;
 };
 
 #endif  // RECOVERY_WEAR_UI_H