[automerger skipped] Import translations. DO NOT MERGE
am: fc8af23e97 -s ours
am skip reason: subject contains skip directive

Change-Id: I4ff4a94f634207aa24c72788a755565798782cba
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..afa0337 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,8 +1,307 @@
-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",
+    ],
+}
+
+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",
+        "libfstab",
+    ],
+
+    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",
+        "libfstab",
+    ],
+}
+
+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",
+        "libziparchive",
+    ],
+
+    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",
+        "libfstab",
+    ],
+
+    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",
+        "libfstab",
+    ],
+
+    init_rc: [
+        "recovery-refresh.rc",
+    ],
+}
+
+filegroup {
+    name: "res-testdata",
+
+    srcs: [
+        "res-*/images/*_text.png",
+    ],
+}
diff --git a/Android.mk b/Android.mk
index 7e0ad12..9806d10 100644
--- a/Android.mk
+++ b/Android.mk
@@ -14,267 +14,65 @@
 
 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 += \
+    make_f2fs.recovery \
+    sload_f2fs.recovery
 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
-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/CleanSpec.mk b/CleanSpec.mk
index e2d97d4..fec823e 100644
--- a/CleanSpec.mk
+++ b/CleanSpec.mk
@@ -49,3 +49,4 @@
 # ************************************************
 $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/EXECUTABLES/recovery_intermediates)
 $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libminui_intermediates/import_includes)
+$(call add-clean-step, rm -rf $(PRODUCT_OUT)/recovery/root/sbin)
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index b5f5f03..28aa06f 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -4,3 +4,7 @@
 [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}
+                  --file_whitelist tools/ updater_sample/
diff --git a/README.md b/README.md
index 0aeadae..efcd318 100644
--- a/README.md
+++ b/README.md
@@ -41,13 +41,6 @@
   contents of pmsg buffer into /data/misc/recovery/inject.txt. Test will pass if
   this file has expected contents.
 
-`ResourceTest` validates whether the png files are qualified as background text
-image under recovery.
-
-    1. `adb sync data` to make sure the test-dir has the images to test.
-    2. The test will automatically pickup and verify all `_text.png` files in
-       the test dir.
-
 Using `adb` under recovery
 --------------------------
 
@@ -60,10 +53,10 @@
     List of devices attached
     1234567890abcdef    recovery
 
-Although `/sbin/adbd` shares the same binary between normal boot and recovery images, only a subset
-of `adb` commands are meaningful under recovery, such as `adb root`, `adb shell`, `adb push`, `adb
-pull` etc. `adb shell` works only after manually mounting `/system` from recovery menu (assuming a
-valid system image on device).
+Although `/system/bin/adbd` is built from the same code base as the one in the normal boot, only a
+subset of `adb` commands are meaningful under recovery, such as `adb root`, `adb shell`, `adb push`,
+`adb pull` etc. Since Android Q, `adb shell` no longer requires manually mounting `/system` from
+recovery menu.
 
 ## Troubleshooting
 
@@ -74,8 +67,8 @@
 
  * Ensure `adbd` is built and running.
 
-By default, `adbd` is always included into recovery image, as `/sbin/adbd`. `init` starts `adbd`
-service automatically only in debuggable builds. This behavior is controlled by the recovery
+By default, `adbd` is always included into recovery image, as `/system/bin/adbd`. `init` starts
+`adbd` service automatically only in debuggable builds. This behavior is controlled by the recovery
 specific `/init.rc`, whose source code is at `bootable/recovery/etc/init.rc`.
 
 The best way to confirm a running `adbd` is by checking the serial output, which shows a service
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..3868ef2 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,171 @@
   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.
+  auto f_bsize = static_cast<int64_t>(sf.f_bsize);
+  auto free_space = sf.f_bsize * sf.f_bavail;
+  if (f_bsize == 0 || free_space / f_bsize != static_cast<int64_t>(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) {
+  // The requested size cannot exceed max int64_t.
+  if (static_cast<uint64_t>(bytes_needed) >
+      static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
+    LOG(ERROR) << "Invalid arg of bytes_needed: " << bytes_needed;
+    return false;
+  }
+
+  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 >= static_cast<int64_t>(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 >= static_cast<int64_t>(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..f4c33e5 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, static_cast<size_t>(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/bootloader_message/bootloader_message.cpp b/bootloader_message/bootloader_message.cpp
index aaeffdc..8c1d63b 100644
--- a/bootloader_message/bootloader_message.cpp
+++ b/bootloader_message/bootloader_message.cpp
@@ -27,21 +27,25 @@
 #include <android-base/properties.h>
 #include <android-base/stringprintf.h>
 #include <android-base/unique_fd.h>
-#include <fs_mgr.h>
+#include <fstab/fstab.h>
+
+using android::fs_mgr::Fstab;
+using android::fs_mgr::ReadDefaultFstab;
 
 static std::string get_misc_blk_device(std::string* err) {
-  std::unique_ptr<fstab, decltype(&fs_mgr_free_fstab)> fstab(fs_mgr_read_fstab_default(),
-                                                             fs_mgr_free_fstab);
-  if (!fstab) {
+  Fstab fstab;
+  if (!ReadDefaultFstab(&fstab)) {
     *err = "failed to read default fstab";
     return "";
   }
-  fstab_rec* record = fs_mgr_get_entry_for_mount_point(fstab.get(), "/misc");
-  if (record == nullptr) {
-    *err = "failed to find /misc partition";
-    return "";
+  for (const auto& entry : fstab) {
+    if (entry.mount_point == "/misc") {
+      return entry.blk_device;
+    }
   }
-  return record->blk_device;
+
+  *err = "failed to find /misc partition";
+  return "";
 }
 
 // In recovery mode, recovery can get started and try to access the misc
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..ce6940a
--- /dev/null
+++ b/fsck_unshare_blocks.cpp
@@ -0,0 +1,151 @@
+/*
+ * 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.
+  std::string system_root = get_system_root();
+  bool mounted = ensure_path_mounted_at(system_root, "/mnt/system") != -1;
+  partitions.push_back(system_root);
+
+  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..9d8943f 100644
--- a/install.cpp
+++ b/install.cpp
@@ -32,9 +32,7 @@
 #include <condition_variable>
 #include <functional>
 #include <limits>
-#include <map>
 #include <mutex>
-#include <string>
 #include <thread>
 #include <vector>
 
@@ -45,13 +43,14 @@
 #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"
@@ -65,18 +64,7 @@
 
 static std::condition_variable finish_log_temperature;
 
-// This function parses and returns the build.version.incremental
-static std::string parse_build_number(const std::string& str) {
-    size_t pos = str.find('=');
-    if (pos != std::string::npos) {
-        return android::base::Trim(str.substr(pos+1));
-    }
-
-    LOG(ERROR) << "Failed to parse build number in " << str;
-    return "";
-}
-
-bool read_metadata_from_package(ZipArchiveHandle zip, std::string* metadata) {
+bool ReadMetadataFromPackage(ZipArchiveHandle zip, std::map<std::string, std::string>* metadata) {
   CHECK(metadata != nullptr);
 
   static constexpr const char* METADATA_PATH = "META-INF/com/android/metadata";
@@ -88,103 +76,79 @@
   }
 
   uint32_t length = entry.uncompressed_length;
-  metadata->resize(length, '\0');
-  int32_t err = ExtractToMemory(zip, &entry, reinterpret_cast<uint8_t*>(&(*metadata)[0]), length);
+  std::string metadata_string(length, '\0');
+  int32_t err =
+      ExtractToMemory(zip, &entry, reinterpret_cast<uint8_t*>(&metadata_string[0]), length);
   if (err != 0) {
     LOG(ERROR) << "Failed to extract " << METADATA_PATH << ": " << ErrorCodeString(err);
     return false;
   }
+
+  for (const std::string& line : android::base::Split(metadata_string, "\n")) {
+    size_t eq = line.find('=');
+    if (eq != std::string::npos) {
+      metadata->emplace(android::base::Trim(line.substr(0, eq)),
+                        android::base::Trim(line.substr(eq + 1)));
+    }
+  }
+
   return true;
 }
 
-// Read the build.version.incremental of src/tgt from the metadata and log it to last_install.
-static void read_source_target_build(ZipArchiveHandle zip, std::vector<std::string>* log_buffer) {
-  std::string metadata;
-  if (!read_metadata_from_package(zip, &metadata)) {
-    return;
-  }
-  // Examples of the pre-build and post-build strings in metadata:
-  //   pre-build-incremental=2943039
-  //   post-build-incremental=2951741
-  std::vector<std::string> lines = android::base::Split(metadata, "\n");
-  for (const std::string& line : lines) {
-    std::string str = android::base::Trim(line);
-    if (android::base::StartsWith(str, "pre-build-incremental")) {
-      std::string source_build = parse_build_number(str);
-      if (!source_build.empty()) {
-        log_buffer->push_back("source_build: " + source_build);
-      }
-    } else if (android::base::StartsWith(str, "post-build-incremental")) {
-      std::string target_build = parse_build_number(str);
-      if (!target_build.empty()) {
-        log_buffer->push_back("target_build: " + target_build);
-      }
-    }
+// Gets the value for the given key in |metadata|. Returns an emtpy string if the key isn't
+// present.
+static std::string get_value(const std::map<std::string, std::string>& metadata,
+                             const std::string& key) {
+  const auto& it = metadata.find(key);
+  return (it == metadata.end()) ? "" : it->second;
+}
+
+static std::string OtaTypeToString(OtaType type) {
+  switch (type) {
+    case OtaType::AB:
+      return "AB";
+    case OtaType::BLOCK:
+      return "BLOCK";
+    case OtaType::BRICK:
+      return "BRICK";
   }
 }
 
-#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.
-static int check_newer_ab_build(ZipArchiveHandle zip) {
-  std::string metadata_str;
-  if (!read_metadata_from_package(zip, &metadata_str)) {
-    return INSTALL_CORRUPT;
-  }
-  std::map<std::string, std::string> metadata;
-  for (const std::string& line : android::base::Split(metadata_str, "\n")) {
-    size_t eq = line.find('=');
-    if (eq != std::string::npos) {
-      metadata[line.substr(0, eq)] = line.substr(eq + 1);
-    }
+// Read the build.version.incremental of src/tgt from the metadata and log it to last_install.
+static void ReadSourceTargetBuild(const std::map<std::string, std::string>& metadata,
+                                  std::vector<std::string>* log_buffer) {
+  // Examples of the pre-build and post-build strings in metadata:
+  //   pre-build-incremental=2943039
+  //   post-build-incremental=2951741
+  auto source_build = get_value(metadata, "pre-build-incremental");
+  if (!source_build.empty()) {
+    log_buffer->push_back("source_build: " + source_build);
   }
 
-  std::string value = android::base::GetProperty("ro.product.device", "");
-  const std::string& pkg_device = metadata["pre-device"];
-  if (pkg_device != value || pkg_device.empty()) {
-    LOG(ERROR) << "Package is for product " << pkg_device << " but expected " << value;
-    return INSTALL_ERROR;
+  auto target_build = get_value(metadata, "post-build-incremental");
+  if (!target_build.empty()) {
+    log_buffer->push_back("target_build: " + target_build);
   }
+}
 
-  // We allow the package to not have any serialno; and we also allow it to carry multiple serial
-  // numbers split by "|"; e.g. serialno=serialno1|serialno2|serialno3 ... We will fail the
-  // verification if the device's serialno doesn't match any of these carried numbers.
-  value = android::base::GetProperty("ro.serialno", "");
-  const std::string& pkg_serial_no = metadata["serialno"];
-  if (!pkg_serial_no.empty()) {
-    bool match = false;
-    for (const std::string& number : android::base::Split(pkg_serial_no, "|")) {
-      if (value == android::base::Trim(number)) {
-        match = true;
-        break;
-      }
-    }
-    if (!match) {
-      LOG(ERROR) << "Package is for serial " << pkg_serial_no;
-      return INSTALL_ERROR;
-    }
-  }
-
-  if (metadata["ota-type"] != "AB") {
-    LOG(ERROR) << "Package is not A/B";
-    return INSTALL_ERROR;
-  }
-
+// Checks the build version, fingerprint and timestamp in the metadata of the A/B package.
+// Downgrading is not allowed unless explicitly enabled in the package and only for
+// incremental packages.
+static int CheckAbSpecificMetadata(const std::map<std::string, std::string>& metadata) {
   // Incremental updates should match the current build.
-  value = android::base::GetProperty("ro.build.version.incremental", "");
-  const std::string& pkg_pre_build = metadata["pre-build-incremental"];
-  if (!pkg_pre_build.empty() && pkg_pre_build != value) {
-    LOG(ERROR) << "Package is for source build " << pkg_pre_build << " but expected " << value;
+  auto device_pre_build = android::base::GetProperty("ro.build.version.incremental", "");
+  auto pkg_pre_build = get_value(metadata, "pre-build-incremental");
+  if (!pkg_pre_build.empty() && pkg_pre_build != device_pre_build) {
+    LOG(ERROR) << "Package is for source build " << pkg_pre_build << " but expected "
+               << device_pre_build;
     return INSTALL_ERROR;
   }
 
-  value = android::base::GetProperty("ro.build.fingerprint", "");
-  const std::string& pkg_pre_build_fingerprint = metadata["pre-build"];
-  if (!pkg_pre_build_fingerprint.empty() && pkg_pre_build_fingerprint != value) {
+  auto device_fingerprint = android::base::GetProperty("ro.build.fingerprint", "");
+  auto pkg_pre_build_fingerprint = get_value(metadata, "pre-build");
+  if (!pkg_pre_build_fingerprint.empty() && pkg_pre_build_fingerprint != device_fingerprint) {
     LOG(ERROR) << "Package is for source build " << pkg_pre_build_fingerprint << " but expected "
-               << value;
+               << device_fingerprint;
     return INSTALL_ERROR;
   }
 
@@ -194,10 +158,11 @@
   int64_t pkg_post_timestamp = 0;
   // We allow to full update to the same version we are running, in case there
   // is a problem with the current copy of that version.
-  if (metadata["post-timestamp"].empty() ||
-      !android::base::ParseInt(metadata["post-timestamp"].c_str(), &pkg_post_timestamp) ||
+  auto pkg_post_timestamp_string = get_value(metadata, "post-timestamp");
+  if (pkg_post_timestamp_string.empty() ||
+      !android::base::ParseInt(pkg_post_timestamp_string, &pkg_post_timestamp) ||
       pkg_post_timestamp < build_timestamp) {
-    if (metadata["ota-downgrade"] != "yes") {
+    if (get_value(metadata, "ota-downgrade") != "yes") {
       LOG(ERROR) << "Update package is older than the current build, expected a build "
                     "newer than timestamp "
                  << build_timestamp << " but package has timestamp " << pkg_post_timestamp
@@ -213,14 +178,55 @@
   return 0;
 }
 
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
-                          const std::string& binary_path, int /* retry_count */, int status_fd,
+int CheckPackageMetadata(const std::map<std::string, std::string>& metadata, OtaType ota_type) {
+  auto package_ota_type = get_value(metadata, "ota-type");
+  auto expected_ota_type = OtaTypeToString(ota_type);
+  if (ota_type != OtaType::AB && ota_type != OtaType::BRICK) {
+    LOG(INFO) << "Skip package metadata check for ota type " << expected_ota_type;
+    return 0;
+  }
+
+  if (package_ota_type != expected_ota_type) {
+    LOG(ERROR) << "Unexpected ota package type, expects " << expected_ota_type << ", actual "
+               << package_ota_type;
+    return INSTALL_ERROR;
+  }
+
+  auto device = android::base::GetProperty("ro.product.device", "");
+  auto pkg_device = get_value(metadata, "pre-device");
+  if (pkg_device != device || pkg_device.empty()) {
+    LOG(ERROR) << "Package is for product " << pkg_device << " but expected " << device;
+    return INSTALL_ERROR;
+  }
+
+  // We allow the package to not have any serialno; and we also allow it to carry multiple serial
+  // numbers split by "|"; e.g. serialno=serialno1|serialno2|serialno3 ... We will fail the
+  // verification if the device's serialno doesn't match any of these carried numbers.
+  auto pkg_serial_no = get_value(metadata, "serialno");
+  if (!pkg_serial_no.empty()) {
+    auto device_serial_no = android::base::GetProperty("ro.serialno", "");
+    bool serial_number_match = false;
+    for (const auto& number : android::base::Split(pkg_serial_no, "|")) {
+      if (device_serial_no == android::base::Trim(number)) {
+        serial_number_match = true;
+      }
+    }
+    if (!serial_number_match) {
+      LOG(ERROR) << "Package is for serial " << pkg_serial_no;
+      return INSTALL_ERROR;
+    }
+  }
+
+  if (ota_type == OtaType::AB) {
+    return CheckAbSpecificMetadata(metadata);
+  }
+
+  return 0;
+}
+
+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);
-  if (ret != 0) {
-    return ret;
-  }
 
   // For A/B updates we extract the payload properties to a buffer and obtain the RAW payload offset
   // in the zip file.
@@ -249,7 +255,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 +264,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 +277,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 +303,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);
@@ -315,24 +318,33 @@
 static int try_update_binary(const std::string& package, ZipArchiveHandle zip, bool* wipe_cache,
                              std::vector<std::string>* log_buffer, int retry_count,
                              int* max_temperature) {
-  read_source_target_build(zip, log_buffer);
+  std::map<std::string, std::string> metadata;
+  if (!ReadMetadataFromPackage(zip, &metadata)) {
+    LOG(ERROR) << "Failed to parse metadata in the zip file";
+    return INSTALL_CORRUPT;
+  }
+
+  bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false);
+  // Verifies against the metadata in the package first.
+  if (int check_status = is_ab ? CheckPackageMetadata(metadata, OtaType::AB) : 0;
+      check_status != 0) {
+    log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
+    return check_status;
+  }
+
+  ReadSourceTargetBuild(metadata, log_buffer);
 
   int pipefd[2];
   pipe(pipefd);
-
   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
-  if (ret) {
+  if (int update_status =
+          is_ab ? SetUpAbUpdateCommands(package, zip, pipefd[1], &args)
+                : SetUpNonAbUpdateCommands(package, zip, retry_count, pipefd[1], &args);
+      update_status != 0) {
     close(pipefd[0]);
     close(pipefd[1]);
     log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure));
-    return ret;
+    return update_status;
   }
 
   // When executing the update binary contained in the package, the
@@ -383,12 +395,8 @@
   //   update attempt.
   //
 
-  // Convert the vector to a NULL-terminated char* array suitable for execv.
-  const char* chr_args[args.size() + 1];
-  chr_args[args.size()] = nullptr;
-  for (size_t i = 0; i < args.size(); i++) {
-    chr_args[i] = args[i].c_str();
-  }
+  // Convert the std::string vector to a NULL-terminated char* vector suitable for execv.
+  auto chr_args = StringVectorToNullTerminatedArray(args);
 
   pid_t pid = fork();
 
@@ -403,7 +411,7 @@
   if (pid == 0) {
     umask(022);
     close(pipefd[0]);
-    execv(chr_args[0], const_cast<char**>(chr_args));
+    execv(chr_args[0], chr_args.data());
     // Bug: 34769056
     // We shouldn't use LOG/PLOG in the forked process, since they may cause
     // the child process to hang. This deadlock results from an improperly
@@ -485,9 +493,16 @@
   if (retry_update) {
     return INSTALL_RETRY;
   }
-  if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
-    LOG(ERROR) << "Error in " << package << " (Status " << WEXITSTATUS(status) << ")";
+  if (WIFEXITED(status)) {
+    if (WEXITSTATUS(status) != EXIT_SUCCESS) {
+      LOG(ERROR) << "Error in " << package << " (status " << WEXITSTATUS(status) << ")";
+      return INSTALL_ERROR;
+    }
+  } else if (WIFSIGNALED(status)) {
+    LOG(ERROR) << "Error in " << package << " (killed by signal " << WTERMSIG(status) << ")";
     return INSTALL_ERROR;
+  } else {
+    LOG(FATAL) << "Invalid status code " << status;
   }
 
   return INSTALL_SUCCESS;
@@ -576,9 +591,9 @@
 
   if (needs_mount) {
     if (path[0] == '@') {
-      ensure_path_mounted(path.substr(1).c_str());
+      ensure_path_mounted(path.substr(1));
     } else {
-      ensure_path_mounted(path.c_str());
+      ensure_path_mounted(path);
     }
   }
 
@@ -627,10 +642,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 +706,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;
   }
@@ -704,18 +718,18 @@
 }
 
 bool verify_package(const unsigned char* package_data, size_t package_size) {
-  static constexpr const char* PUBLIC_KEYS_FILE = "/res/keys";
-  std::vector<Certificate> loadedKeys;
-  if (!load_keys(PUBLIC_KEYS_FILE, loadedKeys)) {
+  static constexpr const char* CERTIFICATE_ZIP_FILE = "/system/etc/security/otacerts.zip";
+  std::vector<Certificate> loaded_keys = LoadKeysFromZipfile(CERTIFICATE_ZIP_FILE);
+  if (loaded_keys.empty()) {
     LOG(ERROR) << "Failed to load keys";
     return false;
   }
-  LOG(INFO) << loadedKeys.size() << " key(s) loaded from " << PUBLIC_KEYS_FILE;
+  LOG(INFO) << loaded_keys.size() << " key(s) loaded from " << CERTIFICATE_ZIP_FILE;
 
   // Verify package.
   ui->Print("Verifying update package...\n");
   auto t0 = std::chrono::system_clock::now();
-  int err = verify_file(package_data, package_size, loadedKeys,
+  int err = verify_file(package_data, package_size, loaded_keys,
                         std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1));
   std::chrono::duration<double> duration = std::chrono::system_clock::now() - t0;
   ui->Print("Update package verification took %.1f s (result %d).\n", duration.count(), err);
diff --git a/install.h b/install.h
index f3fda30..c6db1d1 100644
--- a/install.h
+++ b/install.h
@@ -17,27 +17,49 @@
 #ifndef RECOVERY_INSTALL_H_
 #define RECOVERY_INSTALL_H_
 
+#include <stddef.h>
+
+#include <map>
 #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
+};
+
+enum class OtaType {
+  AB,
+  BLOCK,
+  BRICK,
+};
 
 // 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.
 bool verify_package(const unsigned char* package_data, size_t package_size);
 
-// Read meta data file of the package, write its content in the string pointed by meta_data.
-// Return true if succeed, otherwise return false.
-bool read_metadata_from_package(ZipArchiveHandle zip, std::string* metadata);
+// Reads meta data file of the package; parses each line in the format "key=value"; and writes the
+// result to |metadata|. Return true if succeed, otherwise return false.
+bool ReadMetadataFromPackage(ZipArchiveHandle zip, std::map<std::string, std::string>* metadata);
 
 // Verifies the compatibility info in a Treble-compatible package. Returns true directly if the
 // entry doesn't exist.
 bool verify_package_compatibility(ZipArchiveHandle package_zip);
 
+// Checks if the the metadata in the OTA package has expected values. Returns 0 on success.
+// Mandatory checks: ota-type, pre-device and serial number(if presents)
+// AB OTA specific checks: pre-build version, fingerprint, timestamp.
+int CheckPackageMetadata(const std::map<std::string, std::string>& metadata, OtaType ota_type);
+
 #endif  // RECOVERY_INSTALL_H_
diff --git a/logging.cpp b/logging.cpp
new file mode 100644
index 0000000..283d115
--- /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) != 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..a95d979
--- /dev/null
+++ b/minadbd/Android.bp
@@ -0,0 +1,81 @@
+// 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",
+    ],
+
+    cpp_std: "experimental",
+
+    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..9309ed7 100644
--- a/minadbd/minadbd_services.cpp
+++ b/minadbd/minadbd_services.cpp
@@ -21,57 +21,44 @@
 #include <string.h>
 #include <unistd.h>
 
+#include <functional>
 #include <string>
+#include <string_view>
 #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;
-
-  if (!strncmp(name, "sideload:", 9)) {
-    // this exit status causes recovery to print a special error
-    // message saying to use a newer adb (that supports
-    // sideload-host).
+unique_fd daemon_service_to_fd(std::string_view name, atransport* /* transport */) {
+  if (name.starts_with("sideload:")) {
+    // This exit status causes recovery to print a special error message saying to use a newer adb
+    // (that supports sideload-host).
     exit(3);
-  } else if (!strncmp(name, "sideload-host:", 14)) {
-    std::string arg(name + 14);
-    ret = create_service_thread(sideload_host_service, arg);
+  } else if (name.starts_with("sideload-host:")) {
+    std::string arg(name.substr(strlen("sideload-host:")));
+    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/events.cpp b/minui/events.cpp
index 2894c3b..d94e977 100644
--- a/minui/events.cpp
+++ b/minui/events.cpp
@@ -55,7 +55,7 @@
 }
 
 int ev_init(ev_callback input_cb, bool allow_touch_inputs) {
-  g_epoll_fd = epoll_create(MAX_DEVICES + MAX_MISC_FDS);
+  g_epoll_fd = epoll_create1(EPOLL_CLOEXEC);
   if (g_epoll_fd == -1) {
     return -1;
   }
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..4d1f9b2 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 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,47 +94,56 @@
   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* PixelAt(GRSurface* surface, int x, int y, int row_pixels) {
   switch (rotation) {
-    case ROTATION_NONE:
-      return reinterpret_cast<uint32_t*>(surf->data) + y * row_pixels + x;
-    case ROTATION_RIGHT:
-      return reinterpret_cast<uint32_t*>(surf->data) + x * row_pixels + (surf->width - y);
-    case ROTATION_DOWN:
-      return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - y) * row_pixels +
-             (surf->width - 1 - x);
-    case ROTATION_LEFT:
-      return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - x) * row_pixels + y;
+    case GRRotation::NONE:
+      return reinterpret_cast<uint32_t*>(surface->data()) + y * row_pixels + x;
+    case GRRotation::RIGHT:
+      return reinterpret_cast<uint32_t*>(surface->data()) + x * row_pixels + (surface->width - y);
+    case GRRotation::DOWN:
+      return reinterpret_cast<uint32_t*>(surface->data()) + (surface->height - 1 - y) * row_pixels +
+             (surface->width - 1 - x);
+    case GRRotation::LEFT:
+      return reinterpret_cast<uint32_t*>(surface->data()) + (surface->height - 1 - x) * row_pixels +
+             y;
     default:
-      printf("invalid rotation %d", rotation);
+      printf("invalid rotation %d", static_cast<int>(rotation));
   }
   return nullptr;
 }
 
-static void text_blend(uint8_t* src_p, int src_row_bytes, uint32_t* dst_p, int dst_row_pixels,
-                       int width, int height) {
+static void TextBlend(const uint8_t* src_p, int src_row_bytes, uint32_t* dst_p, int dst_row_pixels,
+                      int width, int height) {
   uint8_t alpha_current = static_cast<uint8_t>((alpha_mask & gr_current) >> 24);
   for (int j = 0; j < height; ++j) {
-    uint8_t* sx = src_p;
+    const uint8_t* sx = src_p;
     uint32_t* px = dst_p;
     for (int i = 0; i < width; ++i, incr_x(&px, dst_row_pixels)) {
       uint8_t a = *sx++;
@@ -152,19 +177,19 @@
     }
 
     int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
-    uint8_t* src_p = font->texture->data + ((ch - ' ') * font->char_width) +
-                     (bold ? font->char_height * font->texture->row_bytes : 0);
-    uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels);
+    const uint8_t* src_p = font->texture->data() + ((ch - ' ') * font->char_width) +
+                           (bold ? font->char_height * font->texture->row_bytes : 0);
+    uint32_t* dst_p = PixelAt(gr_draw, x, y, row_pixels);
 
-    text_blend(src_p, font->texture->row_bytes, dst_p, row_pixels, font->char_width,
-               font->char_height);
+    TextBlend(src_p, font->texture->row_bytes, dst_p, row_pixels, font->char_width,
+              font->char_height);
 
     x += font->char_width;
   }
 }
 
-void gr_texticon(int x, int y, GRSurface* icon) {
-  if (icon == NULL) return;
+void gr_texticon(int x, int y, const GRSurface* icon) {
+  if (icon == nullptr) return;
 
   if (icon->pixel_bytes != 1) {
     printf("gr_texticon: source has wrong format\n");
@@ -177,19 +202,18 @@
   if (outside(x, y) || outside(x + icon->width - 1, y + icon->height - 1)) return;
 
   int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
-  uint8_t* src_p = icon->data;
-  uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels);
-
-  text_blend(src_p, icon->row_bytes, dst_p, row_pixels, icon->width, icon->height);
+  const uint8_t* src_p = icon->data();
+  uint32_t* dst_p = PixelAt(gr_draw, x, y, row_pixels);
+  TextBlend(src_p, icon->row_bytes, dst_p, row_pixels, icon->width, icon->height);
 }
 
 void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
   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() {
@@ -197,9 +221,9 @@
       (gr_current & 0xff) == ((gr_current >> 16) & 0xff) &&
       (gr_current & 0xff) == ((gr_current >> 24) & 0xff) &&
       gr_draw->row_bytes == gr_draw->width * gr_draw->pixel_bytes) {
-    memset(gr_draw->data, gr_current & 0xff, gr_draw->height * gr_draw->row_bytes);
+    memset(gr_draw->data(), gr_current & 0xff, gr_draw->height * gr_draw->row_bytes);
   } else {
-    uint32_t* px = reinterpret_cast<uint32_t*>(gr_draw->data);
+    uint32_t* px = reinterpret_cast<uint32_t*>(gr_draw->data());
     int row_diff = gr_draw->row_bytes / gr_draw->pixel_bytes - gr_draw->width;
     for (int y = 0; y < gr_draw->height; ++y) {
       for (int x = 0; x < gr_draw->width; ++x) {
@@ -220,7 +244,7 @@
   if (outside(x1, y1) || outside(x2 - 1, y2 - 1)) return;
 
   int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
-  uint32_t* p = pixel_at(gr_draw, x1, y1, row_pixels);
+  uint32_t* p = PixelAt(gr_draw, x1, y1, row_pixels);
   uint8_t alpha = static_cast<uint8_t>(((gr_current & alpha_mask) >> 24));
   if (alpha > 0) {
     for (int y = y1; y < y2; ++y) {
@@ -234,8 +258,8 @@
   }
 }
 
-void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
-  if (source == NULL) return;
+void gr_blit(const GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
+  if (source == nullptr) return;
 
   if (gr_draw->pixel_bytes != source->pixel_bytes) {
     printf("gr_blit: source has wrong format\n");
@@ -247,14 +271,15 @@
 
   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;
-    uint32_t* dst_py = pixel_at(gr_draw, dx, dy, row_pixels);
+    const uint32_t* src_py =
+        reinterpret_cast<const uint32_t*>(source->data()) + sy * source->row_bytes / 4 + sx;
+    uint32_t* dst_py = PixelAt(gr_draw, dx, dy, row_pixels);
 
     for (int y = 0; y < h; y += 1) {
-      uint32_t* src_px = src_py;
+      const uint32_t* src_px = src_py;
       uint32_t* dst_px = dst_py;
       for (int x = 0; x < w; x += 1) {
         *dst_px = *src_px++;
@@ -264,11 +289,10 @@
       incr_y(&dst_py, row_pixels);
     }
   } else {
-    unsigned char* src_p = source->data + sy * source->row_bytes + sx * source->pixel_bytes;
-    unsigned char* dst_p = gr_draw->data + dy * gr_draw->row_bytes + dx * gr_draw->pixel_bytes;
+    const uint8_t* src_p = source->data() + sy * source->row_bytes + sx * source->pixel_bytes;
+    uint8_t* 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 +300,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 +337,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 +379,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 +411,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..9eea497 100644
--- a/minui/graphics_adf.cpp
+++ b/minui/graphics_adf.cpp
@@ -20,6 +20,7 @@
 #include <fcntl.h>
 #include <stdio.h>
 #include <stdlib.h>
+#include <string.h>
 #include <sys/mman.h>
 #include <unistd.h>
 
@@ -28,51 +29,60 @@
 
 #include "minui/minui.h"
 
-MinuiBackendAdf::MinuiBackendAdf()
-    : intf_fd(-1), dev(), current_surface(0), n_surfaces(0), surfaces() {}
-
-int MinuiBackendAdf::SurfaceInit(const drm_mode_modeinfo* mode, GRSurfaceAdf* surf) {
-  *surf = {};
-  surf->fence_fd = -1;
-  surf->fd = adf_interface_simple_buffer_alloc(intf_fd, mode->hdisplay, mode->vdisplay, format,
-                                               &surf->offset, &surf->pitch);
-  if (surf->fd < 0) {
-    return surf->fd;
+GRSurfaceAdf::~GRSurfaceAdf() {
+  if (mmapped_buffer_) {
+    munmap(mmapped_buffer_, pitch * height);
   }
-
-  surf->width = mode->hdisplay;
-  surf->height = mode->vdisplay;
-  surf->row_bytes = surf->pitch;
-  surf->pixel_bytes = (format == DRM_FORMAT_RGB565) ? 2 : 4;
-
-  surf->data = static_cast<uint8_t*>(
-      mmap(nullptr, surf->pitch * surf->height, PROT_WRITE, MAP_SHARED, surf->fd, surf->offset));
-  if (surf->data == MAP_FAILED) {
-    int saved_errno = errno;
-    close(surf->fd);
-    return -saved_errno;
+  if (fence_fd != -1) {
+    close(fence_fd);
   }
-
-  return 0;
+  if (fd != -1) {
+    close(fd);
+  }
 }
 
+std::unique_ptr<GRSurfaceAdf> GRSurfaceAdf::Create(int intf_fd, const drm_mode_modeinfo* mode,
+                                                   __u32 format, int* err) {
+  __u32 offset;
+  __u32 pitch;
+  auto fd = adf_interface_simple_buffer_alloc(intf_fd, mode->hdisplay, mode->vdisplay, format,
+                                              &offset, &pitch);
+
+  if (fd < 0) {
+    *err = fd;
+    return nullptr;
+  }
+
+  std::unique_ptr<GRSurfaceAdf> surf = std::unique_ptr<GRSurfaceAdf>(
+      new GRSurfaceAdf(mode->hdisplay, mode->vdisplay, pitch, (format == DRM_FORMAT_RGB565 ? 2 : 4),
+                       offset, pitch, fd));
+
+  auto mmapped =
+      mmap(nullptr, surf->pitch * surf->height, PROT_WRITE, MAP_SHARED, surf->fd, surf->offset);
+  if (mmapped == MAP_FAILED) {
+    *err = -errno;
+    return nullptr;
+  }
+  surf->mmapped_buffer_ = static_cast<uint8_t*>(mmapped);
+  return surf;
+}
+
+MinuiBackendAdf::MinuiBackendAdf() : intf_fd(-1), dev(), current_surface(0), n_surfaces(0) {}
+
 int MinuiBackendAdf::InterfaceInit() {
   adf_interface_data intf_data;
-  int err = adf_get_interface_data(intf_fd, &intf_data);
-  if (err < 0) return err;
+  if (int err = adf_get_interface_data(intf_fd, &intf_data); err < 0) return err;
 
-  int ret = 0;
-  err = SurfaceInit(&intf_data.current_mode, &surfaces[0]);
-  if (err < 0) {
-    fprintf(stderr, "allocating surface 0 failed: %s\n", strerror(-err));
-    ret = err;
+  int result = 0;
+  surfaces[0] = GRSurfaceAdf::Create(intf_fd, &intf_data.current_mode, format, &result);
+  if (!surfaces[0]) {
+    fprintf(stderr, "Failed to allocate surface 0: %s\n", strerror(-result));
     goto done;
   }
 
-  err = SurfaceInit(&intf_data.current_mode, &surfaces[1]);
-  if (err < 0) {
-    fprintf(stderr, "allocating surface 1 failed: %s\n", strerror(-err));
-    surfaces[1] = {};
+  surfaces[1] = GRSurfaceAdf::Create(intf_fd, &intf_data.current_mode, format, &result);
+  if (!surfaces[1]) {
+    fprintf(stderr, "Failed to allocate surface 1: %s\n", strerror(-result));
     n_surfaces = 1;
   } else {
     n_surfaces = 2;
@@ -80,7 +90,7 @@
 
 done:
   adf_free_interface_data(&intf_data);
-  return ret;
+  return result;
 }
 
 int MinuiBackendAdf::DeviceInit(adf_device* dev) {
@@ -104,15 +114,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);
@@ -152,12 +163,12 @@
 }
 
 void MinuiBackendAdf::Sync(GRSurfaceAdf* surf) {
-  static constexpr unsigned int warningTimeout = 3000;
+  static constexpr unsigned int kWarningTimeout = 3000;
 
   if (surf == nullptr) return;
 
   if (surf->fence_fd >= 0) {
-    int err = sync_wait(surf->fence_fd, warningTimeout);
+    int err = sync_wait(surf->fence_fd, kWarningTimeout);
     if (err < 0) {
       perror("adf sync fence wait error\n");
     }
@@ -168,31 +179,22 @@
 }
 
 GRSurface* MinuiBackendAdf::Flip() {
-  GRSurfaceAdf* surf = &surfaces[current_surface];
+  const auto& surf = surfaces[current_surface];
 
   int fence_fd = adf_interface_simple_post(intf_fd, eng_id, surf->width, surf->height, format,
                                            surf->fd, surf->offset, surf->pitch, -1);
   if (fence_fd >= 0) surf->fence_fd = fence_fd;
 
   current_surface = (current_surface + 1) % n_surfaces;
-  Sync(&surfaces[current_surface]);
-  return &surfaces[current_surface];
+  Sync(surfaces[current_surface].get());
+  return surfaces[current_surface].get();
 }
 
 void MinuiBackendAdf::Blank(bool blank) {
   adf_interface_blank(intf_fd, blank ? DRM_MODE_DPMS_OFF : DRM_MODE_DPMS_ON);
 }
 
-void MinuiBackendAdf::SurfaceDestroy(GRSurfaceAdf* surf) {
-  munmap(surf->data, surf->pitch * surf->height);
-  close(surf->fence_fd);
-  close(surf->fd);
-}
-
 MinuiBackendAdf::~MinuiBackendAdf() {
   adf_device_close(&dev);
-  for (unsigned int i = 0; i < n_surfaces; i++) {
-    SurfaceDestroy(&surfaces[i]);
-  }
   if (intf_fd >= 0) close(intf_fd);
 }
diff --git a/minui/graphics_adf.h b/minui/graphics_adf.h
index 2f019ed..79d8d2a 100644
--- a/minui/graphics_adf.h
+++ b/minui/graphics_adf.h
@@ -14,45 +14,63 @@
  * limitations under the License.
  */
 
-#ifndef _GRAPHICS_ADF_H_
-#define _GRAPHICS_ADF_H_
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/types.h>
+
+#include <memory>
 
 #include <adf/adf.h>
 
 #include "graphics.h"
+#include "minui/minui.h"
 
 class GRSurfaceAdf : public GRSurface {
- private:
-  int fence_fd;
-  int fd;
-  __u32 offset;
-  __u32 pitch;
+ public:
+  ~GRSurfaceAdf() override;
 
+  static std::unique_ptr<GRSurfaceAdf> Create(int intf_fd, const drm_mode_modeinfo* mode,
+                                              __u32 format, int* err);
+
+  uint8_t* data() override {
+    return mmapped_buffer_;
+  }
+
+ private:
   friend class MinuiBackendAdf;
+
+  GRSurfaceAdf(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes, __u32 offset,
+               __u32 pitch, int fd)
+      : GRSurface(width, height, row_bytes, pixel_bytes), offset(offset), pitch(pitch), fd(fd) {}
+
+  const __u32 offset;
+  const __u32 pitch;
+
+  int fd;
+  int fence_fd{ -1 };
+  uint8_t* mmapped_buffer_{ nullptr };
 };
 
 class MinuiBackendAdf : public MinuiBackend {
  public:
+  MinuiBackendAdf();
+  ~MinuiBackendAdf() override;
   GRSurface* Init() override;
   GRSurface* Flip() override;
   void Blank(bool) override;
-  ~MinuiBackendAdf() override;
-  MinuiBackendAdf();
 
  private:
-  int SurfaceInit(const drm_mode_modeinfo* mode, GRSurfaceAdf* surf);
   int InterfaceInit();
   int DeviceInit(adf_device* dev);
-  void SurfaceDestroy(GRSurfaceAdf* surf);
   void Sync(GRSurfaceAdf* surf);
 
   int intf_fd;
   adf_id_t eng_id;
   __u32 format;
   adf_device dev;
-  unsigned int current_surface;
-  unsigned int n_surfaces;
-  GRSurfaceAdf surfaces[2];
+  size_t current_surface;
+  size_t n_surfaces;
+  std::unique_ptr<GRSurfaceAdf> surfaces[2];
 };
-
-#endif  // _GRAPHICS_ADF_H_
diff --git a/minui/graphics_drm.cpp b/minui/graphics_drm.cpp
index e7d4b38..765e262 100644
--- a/minui/graphics_drm.cpp
+++ b/minui/graphics_drm.cpp
@@ -17,78 +17,44 @@
 #include "graphics_drm.h"
 
 #include <fcntl.h>
+#include <poll.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sys/mman.h>
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <memory>
+
+#include <android-base/macros.h>
+#include <android-base/stringprintf.h>
+#include <android-base/unique_fd.h>
 #include <drm_fourcc.h>
 #include <xf86drm.h>
 #include <xf86drmMode.h>
 
 #include "minui/minui.h"
 
-#define ARRAY_SIZE(A) (sizeof(A)/sizeof(*(A)))
-
-MinuiBackendDrm::MinuiBackendDrm()
-    : GRSurfaceDrms(), main_monitor_crtc(nullptr), main_monitor_connector(nullptr), drm_fd(-1) {}
-
-void MinuiBackendDrm::DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc) {
-  if (crtc) {
-    drmModeSetCrtc(drm_fd, crtc->crtc_id,
-                   0,         // fb_id
-                   0, 0,      // x,y
-                   nullptr,   // connectors
-                   0,         // connector_count
-                   nullptr);  // mode
-  }
-}
-
-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);
-
-  if (ret) {
-    printf("drmModeSetCrtc failed ret=%d\n", ret);
-  }
-}
-
-void MinuiBackendDrm::Blank(bool blank) {
-  if (blank) {
-    DrmDisableCrtc(drm_fd, main_monitor_crtc);
-  } else {
-    DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[current_buffer]);
-  }
-}
-
-void MinuiBackendDrm::DrmDestroySurface(GRSurfaceDrm* surface) {
-  if (!surface) return;
-
-  if (surface->data) {
-    munmap(surface->data, surface->row_bytes * surface->height);
+GRSurfaceDrm::~GRSurfaceDrm() {
+  if (mmapped_buffer_) {
+    munmap(mmapped_buffer_, row_bytes * height);
   }
 
-  if (surface->fb_id) {
-    int ret = drmModeRmFB(drm_fd, surface->fb_id);
-    if (ret) {
-      printf("drmModeRmFB failed ret=%d\n", ret);
+  if (fb_id) {
+    if (drmModeRmFB(drm_fd_, fb_id) != 0) {
+      perror("Failed to drmModeRmFB");
+      // Falling through to free other resources.
     }
   }
 
-  if (surface->handle) {
+  if (handle) {
     drm_gem_close gem_close = {};
-    gem_close.handle = surface->handle;
+    gem_close.handle = handle;
 
-    int ret = drmIoctl(drm_fd, DRM_IOCTL_GEM_CLOSE, &gem_close);
-    if (ret) {
-      printf("DRM_IOCTL_GEM_CLOSE failed ret=%d\n", ret);
+    if (drmIoctl(drm_fd_, DRM_IOCTL_GEM_CLOSE, &gem_close) != 0) {
+      perror("Failed to DRM_IOCTL_GEM_CLOSE");
     }
   }
-
-  delete surface;
 }
 
 static int drm_format_to_bpp(uint32_t format) {
@@ -108,20 +74,22 @@
   }
 }
 
-GRSurfaceDrm* MinuiBackendDrm::DrmCreateSurface(int width, int height) {
-  GRSurfaceDrm* surface = new GRSurfaceDrm;
-  *surface = {};
-
+std::unique_ptr<GRSurfaceDrm> GRSurfaceDrm::Create(int drm_fd, int width, int height) {
   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;
@@ -129,53 +97,74 @@
   create_dumb.bpp = drm_format_to_bpp(format);
   create_dumb.flags = 0;
 
-  int ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb);
-  if (ret) {
-    printf("DRM_IOCTL_MODE_CREATE_DUMB failed ret=%d\n", ret);
-    DrmDestroySurface(surface);
+  if (drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb) != 0) {
+    perror("Failed to DRM_IOCTL_MODE_CREATE_DUMB");
     return nullptr;
   }
-  surface->handle = create_dumb.handle;
+
+  // Cannot use std::make_unique to access non-public ctor.
+  auto surface = std::unique_ptr<GRSurfaceDrm>(new GRSurfaceDrm(
+      width, height, create_dumb.pitch, create_dumb.bpp / 8, drm_fd, create_dumb.handle));
 
   uint32_t handles[4], pitches[4], offsets[4];
 
   handles[0] = surface->handle;
   pitches[0] = create_dumb.pitch;
   offsets[0] = 0;
-
-  ret =
-      drmModeAddFB2(drm_fd, width, height, format, handles, pitches, offsets, &(surface->fb_id), 0);
-  if (ret) {
-    printf("drmModeAddFB2 failed ret=%d\n", ret);
-    DrmDestroySurface(surface);
+  if (drmModeAddFB2(drm_fd, width, height, format, handles, pitches, offsets, &surface->fb_id, 0) !=
+      0) {
+    perror("Failed to drmModeAddFB2");
     return nullptr;
   }
 
   drm_mode_map_dumb map_dumb = {};
   map_dumb.handle = create_dumb.handle;
-  ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb);
-  if (ret) {
-    printf("DRM_IOCTL_MODE_MAP_DUMB failed ret=%d\n", ret);
-    DrmDestroySurface(surface);
+  if (drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb) != 0) {
+    perror("Failed to DRM_IOCTL_MODE_MAP_DUMB");
     return nullptr;
   }
 
-  surface->height = height;
-  surface->width = width;
-  surface->row_bytes = create_dumb.pitch;
-  surface->pixel_bytes = create_dumb.bpp / 8;
-  surface->data = static_cast<unsigned char*>(mmap(nullptr, surface->height * surface->row_bytes,
-                                                   PROT_READ | PROT_WRITE, MAP_SHARED, drm_fd,
-                                                   map_dumb.offset));
-  if (surface->data == MAP_FAILED) {
-    perror("mmap() failed");
-    DrmDestroySurface(surface);
+  auto mmapped = mmap(nullptr, surface->height * surface->row_bytes, PROT_READ | PROT_WRITE,
+                      MAP_SHARED, drm_fd, map_dumb.offset);
+  if (mmapped == MAP_FAILED) {
+    perror("Failed to mmap()");
     return nullptr;
   }
-
+  surface->mmapped_buffer_ = static_cast<uint8_t*>(mmapped);
   return surface;
 }
 
+void MinuiBackendDrm::DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc) {
+  if (crtc) {
+    drmModeSetCrtc(drm_fd, crtc->crtc_id,
+                   0,         // fb_id
+                   0, 0,      // x,y
+                   nullptr,   // connectors
+                   0,         // connector_count
+                   nullptr);  // mode
+  }
+}
+
+bool MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc,
+                                    const std::unique_ptr<GRSurfaceDrm>& surface) {
+  if (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) != 0) {
+    perror("Failed to drmModeSetCrtc");
+    return false;
+  }
+  return true;
+}
+
+void MinuiBackendDrm::Blank(bool blank) {
+  if (blank) {
+    DrmDisableCrtc(drm_fd, main_monitor_crtc);
+  } else {
+    DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[current_buffer]);
+  }
+}
+
 static drmModeCrtc* find_crtc_for_connector(int fd, drmModeRes* resources,
                                             drmModeConnector* connector) {
   // Find the encoder. If we already have one, just use it.
@@ -257,7 +246,7 @@
   do {
     main_monitor_connector = find_used_connector_by_type(fd, resources, kConnectorPriority[i]);
     i++;
-  } while (!main_monitor_connector && i < ARRAY_SIZE(kConnectorPriority));
+  } while (!main_monitor_connector && i < arraysize(kConnectorPriority));
 
   /* If we didn't find a connector, grab the first one that is connected. */
   if (!main_monitor_connector) {
@@ -291,60 +280,53 @@
 
 GRSurface* MinuiBackendDrm::Init() {
   drmModeRes* res = nullptr;
+  drm_fd = -1;
 
   /* Consider DRM devices in order. */
   for (int i = 0; i < DRM_MAX_MINOR; i++) {
-    char* dev_name;
-    int ret = asprintf(&dev_name, DRM_DEV_NAME, DRM_DIR_NAME, i);
-    if (ret < 0) continue;
+    auto dev_name = android::base::StringPrintf(DRM_DEV_NAME, DRM_DIR_NAME, i);
+    android::base::unique_fd fd(open(dev_name.c_str(), O_RDWR));
+    if (fd == -1) continue;
 
-    drm_fd = open(dev_name, O_RDWR, 0);
-    free(dev_name);
-    if (drm_fd < 0) continue;
-
-    uint64_t cap = 0;
     /* We need dumb buffers. */
-    ret = drmGetCap(drm_fd, DRM_CAP_DUMB_BUFFER, &cap);
-    if (ret || cap == 0) {
-      close(drm_fd);
+    if (uint64_t cap = 0; drmGetCap(fd.get(), DRM_CAP_DUMB_BUFFER, &cap) != 0 || cap == 0) {
       continue;
     }
 
-    res = drmModeGetResources(drm_fd);
+    res = drmModeGetResources(fd.get());
     if (!res) {
-      close(drm_fd);
       continue;
     }
 
     /* Use this device if it has at least one connected monitor. */
     if (res->count_crtcs > 0 && res->count_connectors > 0) {
-      if (find_first_connected_connector(drm_fd, res)) break;
+      if (find_first_connected_connector(fd.get(), res)) {
+        drm_fd = fd.release();
+        break;
+      }
     }
 
     drmModeFreeResources(res);
-    close(drm_fd);
     res = nullptr;
   }
 
-  if (drm_fd < 0 || res == nullptr) {
-    perror("cannot find/open a drm device");
+  if (drm_fd == -1 || res == nullptr) {
+    perror("Failed to find/open a drm device");
     return nullptr;
   }
 
   uint32_t selected_mode;
   main_monitor_connector = FindMainMonitor(drm_fd, res, &selected_mode);
-
   if (!main_monitor_connector) {
-    printf("main_monitor_connector not found\n");
+    fprintf(stderr, "Failed to find main_monitor_connector\n");
     drmModeFreeResources(res);
     close(drm_fd);
     return nullptr;
   }
 
   main_monitor_crtc = find_crtc_for_connector(drm_fd, res, main_monitor_connector);
-
   if (!main_monitor_crtc) {
-    printf("main_monitor_crtc not found\n");
+    fprintf(stderr, "Failed to find main_monitor_crtc\n");
     drmModeFreeResources(res);
     close(drm_fd);
     return nullptr;
@@ -359,35 +341,66 @@
 
   drmModeFreeResources(res);
 
-  GRSurfaceDrms[0] = DrmCreateSurface(width, height);
-  GRSurfaceDrms[1] = DrmCreateSurface(width, height);
+  GRSurfaceDrms[0] = GRSurfaceDrm::Create(drm_fd, width, height);
+  GRSurfaceDrms[1] = GRSurfaceDrm::Create(drm_fd, width, height);
   if (!GRSurfaceDrms[0] || !GRSurfaceDrms[1]) {
-    // GRSurfaceDrms and drm_fd should be freed in d'tor.
     return nullptr;
   }
 
   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])) {
+    return nullptr;
+  }
 
-  return GRSurfaceDrms[0];
+  return GRSurfaceDrms[0].get();
+}
+
+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() {
-  int ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id,
-                            GRSurfaceDrms[current_buffer]->fb_id, 0, nullptr);
-  if (ret < 0) {
-    printf("drmModePageFlip failed ret=%d\n", ret);
+  bool ongoing_flip = true;
+  if (drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id, GRSurfaceDrms[current_buffer]->fb_id,
+                      DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip) != 0) {
+    perror("Failed to drmModePageFlip");
     return nullptr;
   }
+
+  while (ongoing_flip) {
+    struct pollfd fds = {
+      .fd = drm_fd,
+      .events = POLLIN
+    };
+
+    if (poll(&fds, 1, -1) == -1 || !(fds.revents & POLLIN)) {
+      perror("Failed to poll() on drm fd");
+      break;
+    }
+
+    drmEventContext evctx = {
+      .version = DRM_EVENT_CONTEXT_VERSION,
+      .page_flip_handler = page_flip_complete
+    };
+
+    if (drmHandleEvent(drm_fd, &evctx) != 0) {
+      perror("Failed to drmHandleEvent");
+      break;
+    }
+  }
+
   current_buffer = 1 - current_buffer;
-  return GRSurfaceDrms[current_buffer];
+  return GRSurfaceDrms[current_buffer].get();
 }
 
 MinuiBackendDrm::~MinuiBackendDrm() {
   DrmDisableCrtc(drm_fd, main_monitor_crtc);
-  DrmDestroySurface(GRSurfaceDrms[0]);
-  DrmDestroySurface(GRSurfaceDrms[1]);
   drmModeFreeCrtc(main_monitor_crtc);
   drmModeFreeConnector(main_monitor_connector);
   close(drm_fd);
diff --git a/minui/graphics_drm.h b/minui/graphics_drm.h
index de96212..57ba39b 100644
--- a/minui/graphics_drm.h
+++ b/minui/graphics_drm.h
@@ -14,45 +14,61 @@
  * limitations under the License.
  */
 
-#ifndef _GRAPHICS_DRM_H_
-#define _GRAPHICS_DRM_H_
+#pragma once
 
+#include <stddef.h>
 #include <stdint.h>
 
+#include <memory>
+
 #include <xf86drmMode.h>
 
 #include "graphics.h"
 #include "minui/minui.h"
 
 class GRSurfaceDrm : public GRSurface {
- private:
-  uint32_t fb_id;
-  uint32_t handle;
+ public:
+  ~GRSurfaceDrm() override;
 
+  // Creates a GRSurfaceDrm instance.
+  static std::unique_ptr<GRSurfaceDrm> Create(int drm_fd, int width, int height);
+
+  uint8_t* data() override {
+    return mmapped_buffer_;
+  }
+
+ private:
   friend class MinuiBackendDrm;
+
+  GRSurfaceDrm(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes, int drm_fd,
+               uint32_t handle)
+      : GRSurface(width, height, row_bytes, pixel_bytes), drm_fd_(drm_fd), handle(handle) {}
+
+  const int drm_fd_;
+
+  uint32_t fb_id{ 0 };
+  uint32_t handle{ 0 };
+  uint8_t* mmapped_buffer_{ nullptr };
 };
 
 class MinuiBackendDrm : public MinuiBackend {
  public:
+  MinuiBackendDrm() = default;
+  ~MinuiBackendDrm() override;
+
   GRSurface* Init() override;
   GRSurface* Flip() override;
   void Blank(bool) override;
-  ~MinuiBackendDrm() override;
-  MinuiBackendDrm();
 
  private:
   void DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc);
-  void DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface);
-  GRSurfaceDrm* DrmCreateSurface(int width, int height);
-  void DrmDestroySurface(GRSurfaceDrm* surface);
+  bool DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, const std::unique_ptr<GRSurfaceDrm>& surface);
   void DisableNonMainCrtcs(int fd, drmModeRes* resources, drmModeCrtc* main_crtc);
   drmModeConnector* FindMainMonitor(int fd, drmModeRes* resources, uint32_t* mode_index);
 
-  GRSurfaceDrm* GRSurfaceDrms[2];
-  int current_buffer;
-  drmModeCrtc* main_monitor_crtc;
-  drmModeConnector* main_monitor_connector;
-  int drm_fd;
+  std::unique_ptr<GRSurfaceDrm> GRSurfaceDrms[2];
+  int current_buffer{ 0 };
+  drmModeCrtc* main_monitor_crtc{ nullptr };
+  drmModeConnector* main_monitor_connector{ nullptr };
+  int drm_fd{ -1 };
 };
-
-#endif  // _GRAPHICS_DRM_H_
diff --git a/minui/graphics_fbdev.cpp b/minui/graphics_fbdev.cpp
index 746f42a..8d9c974 100644
--- a/minui/graphics_fbdev.cpp
+++ b/minui/graphics_fbdev.cpp
@@ -26,21 +26,29 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <memory>
+
+#include <android-base/unique_fd.h>
+
 #include "minui/minui.h"
 
-MinuiBackendFbdev::MinuiBackendFbdev() : gr_draw(nullptr), fb_fd(-1) {}
+std::unique_ptr<GRSurfaceFbdev> GRSurfaceFbdev::Create(size_t width, size_t height,
+                                                       size_t row_bytes, size_t pixel_bytes) {
+  // Cannot use std::make_unique to access non-public ctor.
+  return std::unique_ptr<GRSurfaceFbdev>(new GRSurfaceFbdev(width, height, row_bytes, pixel_bytes));
+}
 
 void MinuiBackendFbdev::Blank(bool blank) {
   int ret = ioctl(fb_fd, FBIOBLANK, blank ? FB_BLANK_POWERDOWN : FB_BLANK_UNBLANK);
   if (ret < 0) perror("ioctl(): blank");
 }
 
-void MinuiBackendFbdev::SetDisplayedFramebuffer(unsigned n) {
+void MinuiBackendFbdev::SetDisplayedFramebuffer(size_t n) {
   if (n > 1 || !double_buffered) return;
 
-  vi.yres_virtual = gr_framebuffer[0].height * 2;
-  vi.yoffset = n * gr_framebuffer[0].height;
-  vi.bits_per_pixel = gr_framebuffer[0].pixel_bytes * 8;
+  vi.yres_virtual = gr_framebuffer[0]->height * 2;
+  vi.yoffset = n * gr_framebuffer[0]->height;
+  vi.bits_per_pixel = gr_framebuffer[0]->pixel_bytes * 8;
   if (ioctl(fb_fd, FBIOPUT_VSCREENINFO, &vi) < 0) {
     perror("active fb swap failed");
   }
@@ -48,7 +56,7 @@
 }
 
 GRSurface* MinuiBackendFbdev::Init() {
-  int fd = open("/dev/graphics/fb0", O_RDWR);
+  android::base::unique_fd fd(open("/dev/graphics/fb0", O_RDWR));
   if (fd == -1) {
     perror("cannot open fb0");
     return nullptr;
@@ -57,13 +65,11 @@
   fb_fix_screeninfo fi;
   if (ioctl(fd, FBIOGET_FSCREENINFO, &fi) < 0) {
     perror("failed to get fb0 info");
-    close(fd);
     return nullptr;
   }
 
   if (ioctl(fd, FBIOGET_VSCREENINFO, &vi) < 0) {
     perror("failed to get fb0 info");
-    close(fd);
     return nullptr;
   }
 
@@ -90,50 +96,41 @@
   void* bits = mmap(0, fi.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
   if (bits == MAP_FAILED) {
     perror("failed to mmap framebuffer");
-    close(fd);
     return nullptr;
   }
 
   memset(bits, 0, fi.smem_len);
 
-  gr_framebuffer[0].width = vi.xres;
-  gr_framebuffer[0].height = vi.yres;
-  gr_framebuffer[0].row_bytes = fi.line_length;
-  gr_framebuffer[0].pixel_bytes = vi.bits_per_pixel / 8;
-  gr_framebuffer[0].data = static_cast<uint8_t*>(bits);
-  memset(gr_framebuffer[0].data, 0, gr_framebuffer[0].height * gr_framebuffer[0].row_bytes);
+  gr_framebuffer[0] =
+      GRSurfaceFbdev::Create(vi.xres, vi.yres, fi.line_length, vi.bits_per_pixel / 8);
+  gr_framebuffer[0]->buffer_ = static_cast<uint8_t*>(bits);
+  memset(gr_framebuffer[0]->buffer_, 0, gr_framebuffer[0]->height * gr_framebuffer[0]->row_bytes);
+
+  gr_framebuffer[1] =
+      GRSurfaceFbdev::Create(gr_framebuffer[0]->width, gr_framebuffer[0]->height,
+                             gr_framebuffer[0]->row_bytes, gr_framebuffer[0]->pixel_bytes);
 
   /* check if we can use double buffering */
   if (vi.yres * fi.line_length * 2 <= fi.smem_len) {
     double_buffered = true;
 
-    memcpy(gr_framebuffer + 1, gr_framebuffer, sizeof(GRSurface));
-    gr_framebuffer[1].data =
-        gr_framebuffer[0].data + gr_framebuffer[0].height * gr_framebuffer[0].row_bytes;
-
-    gr_draw = gr_framebuffer + 1;
-
+    gr_framebuffer[1]->buffer_ =
+        gr_framebuffer[0]->buffer_ + gr_framebuffer[0]->height * gr_framebuffer[0]->row_bytes;
   } else {
     double_buffered = false;
 
-    // Without double-buffering, we allocate RAM for a buffer to
-    // draw in, and then "flipping" the buffer consists of a
-    // memcpy from the buffer we allocated to the framebuffer.
-
-    gr_draw = static_cast<GRSurface*>(malloc(sizeof(GRSurface)));
-    memcpy(gr_draw, gr_framebuffer, sizeof(GRSurface));
-    gr_draw->data = static_cast<unsigned char*>(malloc(gr_draw->height * gr_draw->row_bytes));
-    if (!gr_draw->data) {
-      perror("failed to allocate in-memory surface");
-      return nullptr;
-    }
+    // Without double-buffering, we allocate RAM for a buffer to draw in, and then "flipping" the
+    // buffer consists of a memcpy from the buffer we allocated to the framebuffer.
+    memory_buffer.resize(gr_framebuffer[1]->height * gr_framebuffer[1]->row_bytes);
+    gr_framebuffer[1]->buffer_ = memory_buffer.data();
   }
 
-  memset(gr_draw->data, 0, gr_draw->height * gr_draw->row_bytes);
-  fb_fd = fd;
+  gr_draw = gr_framebuffer[1].get();
+  memset(gr_draw->buffer_, 0, gr_draw->height * gr_draw->row_bytes);
+  fb_fd = std::move(fd);
   SetDisplayedFramebuffer(0);
 
-  printf("framebuffer: %d (%d x %d)\n", fb_fd, gr_draw->width, gr_draw->height);
+  printf("framebuffer: %d (%zu x %zu)\n", fb_fd.get(), gr_draw->width, gr_draw->height);
 
   Blank(true);
   Blank(false);
@@ -143,25 +140,13 @@
 
 GRSurface* MinuiBackendFbdev::Flip() {
   if (double_buffered) {
-    // Change gr_draw to point to the buffer currently displayed,
-    // then flip the driver so we're displaying the other buffer
-    // instead.
-    gr_draw = gr_framebuffer + displayed_buffer;
+    // Change gr_draw to point to the buffer currently displayed, then flip the driver so we're
+    // displaying the other buffer instead.
+    gr_draw = gr_framebuffer[displayed_buffer].get();
     SetDisplayedFramebuffer(1 - displayed_buffer);
   } else {
     // Copy from the in-memory surface to the framebuffer.
-    memcpy(gr_framebuffer[0].data, gr_draw->data, gr_draw->height * gr_draw->row_bytes);
+    memcpy(gr_framebuffer[0]->buffer_, gr_draw->buffer_, gr_draw->height * gr_draw->row_bytes);
   }
   return gr_draw;
 }
-
-MinuiBackendFbdev::~MinuiBackendFbdev() {
-  close(fb_fd);
-  fb_fd = -1;
-
-  if (!double_buffered && gr_draw) {
-    free(gr_draw->data);
-    free(gr_draw);
-  }
-  gr_draw = nullptr;
-}
diff --git a/minui/graphics_fbdev.h b/minui/graphics_fbdev.h
index 107e195..596ba74 100644
--- a/minui/graphics_fbdev.h
+++ b/minui/graphics_fbdev.h
@@ -14,31 +14,58 @@
  * limitations under the License.
  */
 
-#ifndef _GRAPHICS_FBDEV_H_
-#define _GRAPHICS_FBDEV_H_
+#pragma once
 
 #include <linux/fb.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <memory>
+#include <vector>
+
+#include <android-base/unique_fd.h>
 
 #include "graphics.h"
 #include "minui/minui.h"
 
+class GRSurfaceFbdev : public GRSurface {
+ public:
+  // Creates and returns a GRSurfaceFbdev instance, or nullptr on error.
+  static std::unique_ptr<GRSurfaceFbdev> Create(size_t width, size_t height, size_t row_bytes,
+                                                size_t pixel_bytes);
+
+  uint8_t* data() override {
+    return buffer_;
+  }
+
+ protected:
+  using GRSurface::GRSurface;
+
+ private:
+  friend class MinuiBackendFbdev;
+
+  // Points to the start of the buffer: either the mmap'd framebuffer or one allocated in-memory.
+  uint8_t* buffer_{ nullptr };
+};
+
 class MinuiBackendFbdev : public MinuiBackend {
  public:
+  MinuiBackendFbdev() = default;
+  ~MinuiBackendFbdev() override = default;
+
   GRSurface* Init() override;
   GRSurface* Flip() override;
   void Blank(bool) override;
-  ~MinuiBackendFbdev() override;
-  MinuiBackendFbdev();
 
  private:
-  void SetDisplayedFramebuffer(unsigned n);
+  void SetDisplayedFramebuffer(size_t n);
 
-  GRSurface gr_framebuffer[2];
+  std::unique_ptr<GRSurfaceFbdev> gr_framebuffer[2];
+  // Points to the current surface (i.e. one of the two gr_framebuffer's).
+  GRSurfaceFbdev* gr_draw{ nullptr };
   bool double_buffered;
-  GRSurface* gr_draw;
-  int displayed_buffer;
+  std::vector<uint8_t> memory_buffer;
+  size_t displayed_buffer{ 0 };
   fb_var_screeninfo vi;
-  int fb_fd;
+  android::base::unique_fd fb_fd;
 };
-
-#endif  // _GRAPHICS_FBDEV_H_
diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h
index f9da199..e49c6ac 100644
--- a/minui/include/minui/minui.h
+++ b/minui/include/minui/minui.h
@@ -14,25 +14,72 @@
  * limitations under the License.
  */
 
-#ifndef _MINUI_H_
-#define _MINUI_H_
+#pragma once
 
+#include <stdint.h>
+#include <stdlib.h>
 #include <sys/types.h>
 
 #include <functional>
+#include <memory>
 #include <string>
 #include <vector>
 
+#include <android-base/macros.h>
+
 //
 // Graphics.
 //
 
-struct GRSurface {
-  int width;
-  int height;
-  int row_bytes;
-  int pixel_bytes;
-  unsigned char* data;
+class GRSurface {
+ public:
+  static constexpr size_t kSurfaceDataAlignment = 8;
+
+  virtual ~GRSurface() = default;
+
+  // Creates and returns a GRSurface instance that's sufficient for storing an image of the given
+  // size (i.e. row_bytes * height). The starting address of the surface data is aligned to
+  // kSurfaceDataAlignment. Returns the created GRSurface instance (in std::unique_ptr), or nullptr
+  // on error.
+  static std::unique_ptr<GRSurface> Create(size_t width, size_t height, size_t row_bytes,
+                                           size_t pixel_bytes);
+
+  // Clones the current GRSurface instance (i.e. an image).
+  std::unique_ptr<GRSurface> Clone() const;
+
+  virtual uint8_t* data() {
+    return data_.get();
+  }
+
+  const uint8_t* data() const {
+    return const_cast<const uint8_t*>(const_cast<GRSurface*>(this)->data());
+  }
+
+  size_t data_size() const {
+    return data_size_;
+  }
+
+  size_t width;
+  size_t height;
+  size_t row_bytes;
+  size_t pixel_bytes;
+
+ protected:
+  GRSurface(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes)
+      : width(width), height(height), row_bytes(row_bytes), pixel_bytes(pixel_bytes) {}
+
+ private:
+  // The deleter for data_, whose data is allocated via aligned_alloc(3).
+  struct DataDeleter {
+    void operator()(uint8_t* data) {
+      free(data);
+    }
+  };
+
+  std::unique_ptr<uint8_t, DataDeleter> data_;
+  size_t data_size_;
+
+  DISALLOW_COPY_AND_ASSIGN(GRSurface);
 };
 
 struct GRFont {
@@ -41,14 +88,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,25 +117,31 @@
 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);
 
-void gr_texticon(int x, int y, GRSurface* icon);
+void gr_texticon(int x, int y, const GRSurface* icon);
 
 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);
+void gr_blit(const GRSurface* source, int sx, int sy, int w, int h, int dx, int dy);
+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.
 //
@@ -146,5 +212,3 @@
 // Free a surface allocated by any of the res_create_*_surface()
 // functions.
 void res_free_surface(GRSurface* surface);
-
-#endif
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..069a495 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>
@@ -25,87 +27,55 @@
 #include <sys/types.h>
 #include <unistd.h>
 
+#include <limits>
 #include <memory>
 #include <regex>
 #include <string>
 #include <vector>
 
-#include <android-base/stringprintf.h>
 #include <android-base/strings.h>
 #include <png.h>
 
 #include "minui/minui.h"
 
-#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));
-    if (temp == NULL) return NULL;
-    GRSurface* surface = reinterpret_cast<GRSurface*>(temp);
-    surface->data = temp + sizeof(GRSurface) +
-        (SURFACE_DATA_ALIGNMENT - (sizeof(GRSurface) % SURFACE_DATA_ALIGNMENT));
-    return surface;
+std::unique_ptr<GRSurface> GRSurface::Create(size_t width, size_t height, size_t row_bytes,
+                                             size_t pixel_bytes) {
+  if (width == 0 || row_bytes == 0 || height == 0 || pixel_bytes == 0) return nullptr;
+  if (std::numeric_limits<size_t>::max() / row_bytes < height) return nullptr;
+
+  // Cannot use std::make_unique to access non-public ctor.
+  auto result = std::unique_ptr<GRSurface>(new GRSurface(width, height, row_bytes, pixel_bytes));
+  size_t data_size = row_bytes * height;
+  result->data_size_ =
+      (data_size + kSurfaceDataAlignment - 1) / kSurfaceDataAlignment * kSurfaceDataAlignment;
+  result->data_.reset(
+      static_cast<uint8_t*>(aligned_alloc(kSurfaceDataAlignment, result->data_size_)));
+  if (!result->data_) return nullptr;
+  return result;
 }
 
-// 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);
+std::unique_ptr<GRSurface> GRSurface::Clone() const {
+  auto result = GRSurface::Create(width, height, row_bytes, pixel_bytes);
+  if (!result) return nullptr;
+  memcpy(result->data(), data(), data_size_);
+  return result;
+}
 
-  ~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;
   }
 
-  unsigned char header[8];
+  uint8_t header[8];
   size_t bytesRead = fread(header, 1, sizeof(header), png_fp_.get());
   if (bytesRead != sizeof(header)) {
     error_code_ = -2;
@@ -138,19 +108,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 +126,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;
   }
 }
@@ -170,70 +138,49 @@
   }
 }
 
-// "display" surfaces are transformed into the framebuffer's required
-// pixel format (currently only RGBX is supported) at load time, so
-// gr_blit() can be nothing more than a memcpy() for each row.  The
-// next two functions are the only ones that know anything about the
-// framebuffer pixel format; they need to be modified if the
-// framebuffer format changes (but nothing else should).
+// "display" surfaces are transformed into the framebuffer's required pixel format (currently only
+// RGBX is supported) at load time, so gr_blit() can be nothing more than a memcpy() for each row.
 
-// Allocate and return a GRSurface* sufficient for storing an image of
-// the indicated size in the framebuffer pixel format.
-static GRSurface* init_display_surface(png_uint_32 width, png_uint_32 height) {
-    GRSurface* surface = malloc_surface(width * height * 4);
-    if (surface == NULL) return NULL;
-
-    surface->width = width;
-    surface->height = height;
-    surface->row_bytes = width * 4;
-    surface->pixel_bytes = 4;
-
-    return surface;
-}
-
-// Copy 'input_row' to 'output_row', transforming it to the
-// framebuffer pixel format.  The input format depends on the value of
-// 'channels':
+// Copies 'input_row' to 'output_row', transforming it to the framebuffer pixel format. The input
+// format depends on the value of 'channels':
 //
 //   1 - input is 8-bit grayscale
 //   3 - input is 24-bit RGB
 //   4 - input is 32-bit RGBA/RGBX
 //
 // 'width' is the number of pixels in the row.
-static void transform_rgb_to_draw(unsigned char* input_row,
-                                  unsigned char* output_row,
-                                  int channels, int width) {
-    int x;
-    unsigned char* ip = input_row;
-    unsigned char* op = output_row;
+static void TransformRgbToDraw(const uint8_t* input_row, uint8_t* output_row, int channels,
+                               int width) {
+  const uint8_t* ip = input_row;
+  uint8_t* op = output_row;
 
-    switch (channels) {
-        case 1:
-            // expand gray level to RGBX
-            for (x = 0; x < width; ++x) {
-                *op++ = *ip;
-                *op++ = *ip;
-                *op++ = *ip;
-                *op++ = 0xff;
-                ip++;
-            }
-            break;
+  switch (channels) {
+    case 1:
+      // expand gray level to RGBX
+      for (int x = 0; x < width; ++x) {
+        *op++ = *ip;
+        *op++ = *ip;
+        *op++ = *ip;
+        *op++ = 0xff;
+        ip++;
+      }
+      break;
 
-        case 3:
-            // expand RGBA to RGBX
-            for (x = 0; x < width; ++x) {
-                *op++ = *ip++;
-                *op++ = *ip++;
-                *op++ = *ip++;
-                *op++ = 0xff;
-            }
-            break;
+    case 3:
+      // expand RGBA to RGBX
+      for (int x = 0; x < width; ++x) {
+        *op++ = *ip++;
+        *op++ = *ip++;
+        *op++ = *ip++;
+        *op++ = 0xff;
+      }
+      break;
 
-        case 4:
-            // copy RGBA to RGBX
-            memcpy(output_row, input_row, width*4);
-            break;
-    }
+    case 4:
+      // copy RGBA to RGBX
+      memcpy(output_row, input_row, width * 4);
+      break;
+  }
 }
 
 int res_create_display_surface(const char* name, GRSurface** pSurface) {
@@ -246,23 +193,24 @@
   png_uint_32 width = png_handler.width();
   png_uint_32 height = png_handler.height();
 
-  GRSurface* surface = init_display_surface(width, height);
+  auto surface = GRSurface::Create(width, height, width * 4, 4);
   if (!surface) {
     return -8;
   }
 
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-  png_set_bgr(png_ptr);
-#endif
-
-  for (png_uint_32 y = 0; y < height; ++y) {
-    std::vector<unsigned char> p_row(width * 4);
-    png_read_row(png_ptr, p_row.data(), nullptr);
-    transform_rgb_to_draw(p_row.data(), surface->data + y * surface->row_bytes,
-                          png_handler.channels(), width);
+  PixelFormat pixel_format = gr_pixel_format();
+  if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+    png_set_bgr(png_ptr);
   }
 
-  *pSurface = surface;
+  for (png_uint_32 y = 0; y < height; ++y) {
+    std::vector<uint8_t> p_row(width * 4);
+    png_read_row(png_ptr, p_row.data(), nullptr);
+    TransformRgbToDraw(p_row.data(), surface->data() + y * surface->row_bytes,
+                       png_handler.channels(), width);
+  }
+
+  *pSurface = surface.release();
 
   return 0;
 }
@@ -315,23 +263,24 @@
     goto exit;
   }
   for (int i = 0; i < *frames; ++i) {
-    surface[i] = init_display_surface(width, height / *frames);
-    if (!surface[i]) {
+    auto created_surface = GRSurface::Create(width, height / *frames, width * 4, 4);
+    if (!created_surface) {
       result = -8;
       goto exit;
     }
+    surface[i] = created_surface.release();
   }
 
-#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);
+    std::vector<uint8_t> p_row(width * 4);
     png_read_row(png_ptr, p_row.data(), nullptr);
     int frame = y % *frames;
-    unsigned char* out_row = surface[frame]->data + (y / *frames) * surface[frame]->row_bytes;
-    transform_rgb_to_draw(p_row.data(), out_row, png_handler.channels(), width);
+    uint8_t* out_row = surface[frame]->data() + (y / *frames) * surface[frame]->row_bytes;
+    TransformRgbToDraw(p_row.data(), out_row, png_handler.channels(), width);
   }
 
   *pSurface = surface;
@@ -362,29 +311,30 @@
   png_uint_32 width = png_handler.width();
   png_uint_32 height = png_handler.height();
 
-  GRSurface* surface = malloc_surface(width * height);
+  auto surface = GRSurface::Create(width, height, width, 1);
   if (!surface) {
     return -8;
   }
-  surface->width = width;
-  surface->height = height;
-  surface->row_bytes = width;
-  surface->pixel_bytes = 1;
 
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
-  png_set_bgr(png_ptr);
-#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;
+    uint8_t* p_row = surface->data() + y * surface->row_bytes;
     png_read_row(png_ptr, p_row, nullptr);
   }
 
-  *pSurface = surface;
+  *pSurface = surface.release();
 
   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) {
@@ -421,7 +371,7 @@
   }
 
   std::vector<std::string> result;
-  std::vector<unsigned char> row(png_handler.width());
+  std::vector<uint8_t> row(png_handler.width());
   for (png_uint_32 y = 0; y < png_handler.height(); ++y) {
     png_read_row(png_handler.png_ptr(), row.data(), nullptr);
     int h = (row[3] << 8) | row[2];
@@ -457,7 +407,7 @@
   png_uint_32 height = png_handler.height();
 
   for (png_uint_32 y = 0; y < height; ++y) {
-    std::vector<unsigned char> row(width);
+    std::vector<uint8_t> row(width);
     png_read_row(png_ptr, row.data(), nullptr);
     int w = (row[1] << 8) | row[0];
     int h = (row[3] << 8) | row[2];
@@ -467,21 +417,17 @@
     if (y + 1 + h >= height || matches_locale(loc, locale)) {
       printf("  %20s: %s (%d x %d @ %d)\n", name, loc, w, h, y);
 
-      GRSurface* surface = malloc_surface(w * h);
+      auto surface = GRSurface::Create(w, h, w, 1);
       if (!surface) {
         return -8;
       }
-      surface->width = w;
-      surface->height = h;
-      surface->row_bytes = w;
-      surface->pixel_bytes = 1;
 
       for (int i = 0; i < h; ++i, ++y) {
         png_read_row(png_ptr, row.data(), nullptr);
-        memcpy(surface->data + i * w, row.data(), w);
+        memcpy(surface->data() + i * w, row.data(), w);
       }
 
-      *pSurface = surface;
+      *pSurface = surface.release();
       break;
     }
 
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..e2a6699 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 (access(LAST_INSTALL_FILE_IN_CACHE, F_OK) && 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 (access(LAST_INSTALL_FILE, F_OK) && 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..703923e 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,67 +279,22 @@
     // 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.
 static std::string browse_directory(const std::string& path, Device* device) {
-  ensure_path_mounted(path.c_str());
+  ensure_path_mounted(path);
 
   std::unique_ptr<DIR, decltype(&closedir)> d(opendir(path.c_str()), closedir);
   if (!d) {
@@ -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,24 @@
 }
 
 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!");
+  std::vector<std::string> headers{ "Wipe all user data?", "  THIS CAN NOT BE UNDONE!" };
+  std::vector<std::string> items{ " Cancel", " Factory data reset" };
+
+  size_t chosen_item = ui->ShowPromptWipeDataConfirmationMenu(
+      headers, items,
+      std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+  return (chosen_item == 1);
 }
 
 // Return true on success.
@@ -770,27 +401,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> wipe_data_menu_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> wipe_data_menu_items {
     "Try again",
     "Factory data reset",
-    NULL
   };
+  // clang-format on
   for (;;) {
-    int chosen_item = get_menu_selection(headers, items, true, 0, device);
-    if (chosen_item != 1) {
-      return true;  // Just reboot, no wipe; not a failure, user asked for it
+    size_t chosen_item = ui->ShowPromptWipeDataMenu(
+        wipe_data_menu_headers, wipe_data_menu_items,
+        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 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;
+      }
     }
   }
 }
@@ -883,64 +525,47 @@
         LOG(ERROR) << "Can't open wipe package : " << ErrorCodeString(err);
         return false;
     }
-    std::string metadata;
-    if (!read_metadata_from_package(zip, &metadata)) {
-        CloseArchive(zip);
-        return false;
+
+    std::map<std::string, std::string> metadata;
+    if (!ReadMetadataFromPackage(zip, &metadata)) {
+      LOG(ERROR) << "Failed to parse metadata in the zip file";
+      return false;
     }
+
+    int result = CheckPackageMetadata(metadata, OtaType::BRICK);
     CloseArchive(zip);
 
-    // Check metadata
-    std::vector<std::string> lines = android::base::Split(metadata, "\n");
-    bool ota_type_matched = false;
-    bool device_type_matched = false;
-    bool has_serial_number = false;
-    bool serial_number_matched = false;
-    for (const auto& line : lines) {
-        if (line == "ota-type=BRICK") {
-            ota_type_matched = true;
-        } else if (android::base::StartsWith(line, "pre-device=")) {
-            std::string device_type = line.substr(strlen("pre-device="));
-            std::string real_device_type = android::base::GetProperty("ro.build.product", "");
-            device_type_matched = (device_type == real_device_type);
-        } else if (android::base::StartsWith(line, "serialno=")) {
-            std::string serial_no = line.substr(strlen("serialno="));
-            std::string real_serial_no = android::base::GetProperty("ro.serialno", "");
-            has_serial_number = true;
-            serial_number_matched = (serial_no == real_serial_no);
-        }
-    }
-    return ota_type_matched && device_type_matched && (!has_serial_number || serial_number_matched);
+    return result == 0;
 }
 
-// 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) {
@@ -953,7 +578,7 @@
           log_file += "." + std::to_string(i);
         }
 
-        if (ensure_path_mounted(log_file.c_str()) == 0 && access(log_file.c_str(), R_OK) == 0) {
+        if (ensure_path_mounted(log_file) == 0 && access(log_file.c_str(), R_OK) == 0) {
           entries.push_back(std::move(log_file));
         }
       };
@@ -966,28 +591,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 +718,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 +757,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 +779,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 +804,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 +818,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 +837,18 @@
 
       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)
-        if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
-          if (ensure_path_mounted_at("/", "/system_root") != -1) {
-            ui->Print("Mounted /system.\n");
-          }
-        } else {
-          if (ensure_path_mounted("/system") != -1) {
-            ui->Print("Mounted /system.\n");
-          }
+        // the system partition is mounted at /mnt/system
+        if (ensure_path_mounted_at(get_system_root(), "/mnt/system") != -1) {
+          ui->Print("Mounted /system.\n");
         }
         break;
+
+      case Device::KEY_INTERRUPTED:
+        return Device::KEY_INTERRUPTED;
     }
   }
 }
@@ -1227,21 +857,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 +871,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 +903,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 +930,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 +961,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 +1017,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 +1081,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 +1095,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 +1121,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 +1141,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 +1180,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 +1207,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 +1217,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 +1257,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..6f50802
--- /dev/null
+++ b/recovery_main.cpp
@@ -0,0 +1,504 @@
+/*
+ * 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.dynamic_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.dynamic_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:
+        if (logical_partitions_mapped()) {
+          ui->Print("Partitions may be mounted - rebooting to enter fastboot.");
+          android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,fastboot");
+        } else {
+          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..7a922b8 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -18,6 +18,7 @@
 
 #include <ctype.h>
 #include <fcntl.h>
+#include <inttypes.h>
 #include <stdint.h>
 #include <stdlib.h>
 #include <string.h>
@@ -27,7 +28,7 @@
 #include <sys/wait.h>
 #include <unistd.h>
 
-#include <algorithm>
+#include <iostream>
 #include <string>
 #include <vector>
 
@@ -38,155 +39,60 @@
 #include <cryptfs.h>
 #include <ext4_utils/wipe.h>
 #include <fs_mgr.h>
+#include <fs_mgr/roots.h>
+#include <fs_mgr_dm_linear.h>
 
-#include "mounts.h"
+#include "otautil/mounts.h"
+#include "otautil/sysutil.h"
 
-static struct fstab* fstab = nullptr;
+using android::fs_mgr::Fstab;
+using android::fs_mgr::FstabEntry;
+using android::fs_mgr::ReadDefaultFstab;
+
+static Fstab fstab;
 
 extern struct selabel_handle* sehandle;
 
 void load_volume_table() {
-  fstab = fs_mgr_read_fstab_default();
-  if (!fstab) {
+  if (!ReadDefaultFstab(&fstab)) {
     LOG(ERROR) << "Failed to read default fstab";
     return;
   }
 
-  int ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
-  if (ret == -1) {
-    LOG(ERROR) << "Failed to add /tmp entry to fstab";
-    fs_mgr_free_fstab(fstab);
-    fstab = nullptr;
-    return;
-  }
+  fstab.emplace_back(FstabEntry{
+      .mount_point = "/tmp", .fs_type = "ramdisk", .blk_device = "ramdisk", .length = 0 });
 
-  printf("recovery filesystem table\n");
-  printf("=========================\n");
-  for (int i = 0; i < fstab->num_entries; ++i) {
-    const Volume* v = &fstab->recs[i];
-    printf("  %d %s %s %s %lld\n", i, v->mount_point, v->fs_type, v->blk_device, v->length);
+  std::cout << "recovery filesystem table" << std::endl << "=========================" << std::endl;
+  for (size_t i = 0; i < fstab.size(); ++i) {
+    const auto& entry = fstab[i];
+    std::cout << "  " << i << " " << entry.mount_point << " "
+              << " " << entry.fs_type << " " << entry.blk_device << " " << entry.length
+              << std::endl;
   }
-  printf("\n");
+  std::cout << std::endl;
 }
 
 Volume* volume_for_mount_point(const std::string& mount_point) {
-  return fs_mgr_get_entry_for_mount_point(fstab, mount_point);
-}
-
-// Finds the volume specified by the given path. fs_mgr_get_entry_for_mount_point() does exact match
-// only, so it attempts the prefixes recursively (e.g. "/cache/recovery/last_log",
-// "/cache/recovery", "/cache", "/" for a given path of "/cache/recovery/last_log") and returns the
-// first match or nullptr.
-static Volume* volume_for_path(const char* path) {
-  if (path == nullptr || path[0] == '\0') return nullptr;
-  std::string str(path);
-  while (true) {
-    Volume* result = fs_mgr_get_entry_for_mount_point(fstab, str);
-    if (result != nullptr || str == "/") {
-      return result;
-    }
-    size_t slash = str.find_last_of('/');
-    if (slash == std::string::npos) return nullptr;
-    if (slash == 0) {
-      str = "/";
-    } else {
-      str = str.substr(0, slash);
-    }
-  }
-  return nullptr;
+  return android::fs_mgr::GetEntryForMountPoint(&fstab, mount_point);
 }
 
 // Mount the volume specified by path at the given mount_point.
-int ensure_path_mounted_at(const char* path, const char* mount_point) {
-  Volume* v = volume_for_path(path);
-  if (v == nullptr) {
-    LOG(ERROR) << "unknown volume for path [" << path << "]";
-    return -1;
-  }
-  if (strcmp(v->fs_type, "ramdisk") == 0) {
-    // The ramdisk is always mounted.
-    return 0;
-  }
-
-  if (!scan_mounted_volumes()) {
-    LOG(ERROR) << "Failed to scan mounted volumes";
-    return -1;
-  }
-
-  if (!mount_point) {
-    mount_point = v->mount_point;
-  }
-
-  const MountedVolume* mv = find_mounted_volume_by_mount_point(mount_point);
-  if (mv != nullptr) {
-    // Volume is already mounted.
-    return 0;
-  }
-
-  mkdir(mount_point, 0755);  // in case it doesn't already exist
-
-  if (strcmp(v->fs_type, "ext4") == 0 || strcmp(v->fs_type, "squashfs") == 0 ||
-      strcmp(v->fs_type, "vfat") == 0) {
-    int result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options);
-    if (result == -1 && fs_mgr_is_formattable(v)) {
-      PLOG(ERROR) << "Failed to mount " << mount_point << "; formatting";
-      bool crypt_footer = fs_mgr_is_encryptable(v) && !strcmp(v->key_loc, "footer");
-      if (fs_mgr_do_format(v, crypt_footer) == 0) {
-        result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options);
-      } else {
-        PLOG(ERROR) << "Failed to format " << mount_point;
-        return -1;
-      }
-    }
-
-    if (result == -1) {
-      PLOG(ERROR) << "Failed to mount " << mount_point;
-      return -1;
-    }
-    return 0;
-  }
-
-  LOG(ERROR) << "unknown fs_type \"" << v->fs_type << "\" for " << mount_point;
-  return -1;
+int ensure_path_mounted_at(const std::string& path, const std::string& mount_point) {
+  return android::fs_mgr::EnsurePathMounted(&fstab, path, mount_point) ? 0 : -1;
 }
 
-int ensure_path_mounted(const char* path) {
+int ensure_path_mounted(const std::string& path) {
   // Mount at the default mount point.
-  return ensure_path_mounted_at(path, nullptr);
+  return android::fs_mgr::EnsurePathMounted(&fstab, path) ? 0 : -1;
 }
 
-int ensure_path_unmounted(const char* path) {
-  const Volume* v = volume_for_path(path);
-  if (v == nullptr) {
-    LOG(ERROR) << "unknown volume for path [" << path << "]";
-    return -1;
-  }
-  if (strcmp(v->fs_type, "ramdisk") == 0) {
-    // The ramdisk is always mounted; you can't unmount it.
-    return -1;
-  }
-
-  if (!scan_mounted_volumes()) {
-    LOG(ERROR) << "Failed to scan mounted volumes";
-    return -1;
-  }
-
-  MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point);
-  if (mv == nullptr) {
-    // Volume is already unmounted.
-    return 0;
-  }
-
-  return unmount_mounted_volume(mv);
+int ensure_path_unmounted(const std::string& path) {
+  return android::fs_mgr::EnsurePathUnmounted(&fstab, path) ? 0 : -1;
 }
 
 static int exec_cmd(const std::vector<std::string>& args) {
-  CHECK_NE(static_cast<size_t>(0), args.size());
-
-  std::vector<char*> argv(args.size());
-  std::transform(args.cbegin(), args.cend(), argv.begin(),
-                 [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
-  argv.push_back(nullptr);
+  CHECK(!args.empty());
+  auto argv = StringVectorToNullTerminatedArray(args);
 
   pid_t child;
   if ((child = fork()) == 0) {
@@ -225,17 +131,17 @@
   return computed_size;
 }
 
-int format_volume(const char* volume, const char* directory) {
-  const Volume* v = volume_for_path(volume);
+int format_volume(const std::string& volume, const std::string& directory) {
+  const FstabEntry* v = android::fs_mgr::GetEntryForPath(&fstab, volume);
   if (v == nullptr) {
     LOG(ERROR) << "unknown volume \"" << volume << "\"";
     return -1;
   }
-  if (strcmp(v->fs_type, "ramdisk") == 0) {
+  if (v->fs_type == "ramdisk") {
     LOG(ERROR) << "can't format_volume \"" << volume << "\"";
     return -1;
   }
-  if (strcmp(v->mount_point, volume) != 0) {
+  if (v->mount_point != volume) {
     LOG(ERROR) << "can't give path \"" << volume << "\" to format_volume";
     return -1;
   }
@@ -243,16 +149,16 @@
     LOG(ERROR) << "format_volume: Failed to unmount \"" << v->mount_point << "\"";
     return -1;
   }
-  if (strcmp(v->fs_type, "ext4") != 0 && strcmp(v->fs_type, "f2fs") != 0) {
+  if (v->fs_type != "ext4" && v->fs_type != "f2fs") {
     LOG(ERROR) << "format_volume: fs_type \"" << v->fs_type << "\" unsupported";
     return -1;
   }
 
   // If there's a key_loc that looks like a path, it should be a block device for storing encryption
   // metadata. Wipe it too.
-  if (v->key_loc != nullptr && v->key_loc[0] == '/') {
+  if (!v->key_loc.empty() && v->key_loc[0] == '/') {
     LOG(INFO) << "Wiping " << v->key_loc;
-    int fd = open(v->key_loc, O_WRONLY | O_CREAT, 0644);
+    int fd = open(v->key_loc.c_str(), O_WRONLY | O_CREAT, 0644);
     if (fd == -1) {
       PLOG(ERROR) << "format_volume: Failed to open " << v->key_loc;
       return -1;
@@ -264,9 +170,8 @@
   int64_t length = 0;
   if (v->length > 0) {
     length = v->length;
-  } else if (v->length < 0 ||
-             (v->key_loc != nullptr && strcmp(v->key_loc, "footer") == 0)) {
-    android::base::unique_fd fd(open(v->blk_device, O_RDONLY));
+  } else if (v->length < 0 || v->key_loc == "footer") {
+    android::base::unique_fd fd(open(v->blk_device.c_str(), O_RDONLY));
     if (fd == -1) {
       PLOG(ERROR) << "format_volume: failed to open " << v->blk_device;
       return -1;
@@ -280,10 +185,10 @@
     }
   }
 
-  if (strcmp(v->fs_type, "ext4") == 0) {
+  if (v->fs_type == "ext4") {
     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;
@@ -303,15 +208,9 @@
     }
 
     int result = exec_cmd(mke2fs_args);
-    if (result == 0 && directory != nullptr) {
+    if (result == 0 && !directory.empty()) {
       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);
     }
@@ -325,71 +224,66 @@
 
   // Has to be f2fs because we checked earlier.
   static constexpr int kSectorSize = 4096;
-  std::string cmd("/sbin/mkfs.f2fs");
-  // clang-format off
   std::vector<std::string> make_f2fs_cmd = {
-    cmd,
-    "-d1",
-    "-f",
-    "-O", "encrypt",
-    "-O", "quota",
-    "-O", "verity",
-    "-w", std::to_string(kSectorSize),
+    "/system/bin/make_f2fs",
+    "-g",
+    "android",
     v->blk_device,
   };
-  // clang-format on
   if (length >= kSectorSize) {
     make_f2fs_cmd.push_back(std::to_string(length / kSectorSize));
   }
 
-  int result = exec_cmd(make_f2fs_cmd);
-  if (result == 0 && directory != nullptr) {
-    cmd = "/sbin/sload.f2fs";
-    // clang-format off
-    std::vector<std::string> sload_f2fs_cmd = {
-      cmd,
-      "-f", directory,
-      "-t", volume,
-      v->blk_device,
-    };
-    // clang-format on
-    result = exec_cmd(sload_f2fs_cmd);
-  }
-  if (result != 0) {
-    PLOG(ERROR) << "format_volume: Failed " << cmd << " on " << v->blk_device;
+  if (exec_cmd(make_f2fs_cmd) != 0) {
+    PLOG(ERROR) << "format_volume: Failed to make_f2fs on " << v->blk_device;
     return -1;
   }
+  if (!directory.empty()) {
+    std::vector<std::string> sload_f2fs_cmd = {
+      "/system/bin/sload_f2fs", "-f", directory, "-t", volume, v->blk_device,
+    };
+    if (exec_cmd(sload_f2fs_cmd) != 0) {
+      PLOG(ERROR) << "format_volume: Failed to sload_f2fs on " << v->blk_device;
+      return -1;
+    }
+  }
   return 0;
 }
 
-int format_volume(const char* volume) {
-  return format_volume(volume, nullptr);
+int format_volume(const std::string& volume) {
+  return format_volume(volume, "");
 }
 
 int setup_install_mounts() {
-  if (fstab == nullptr) {
+  if (fstab.empty()) {
     LOG(ERROR) << "can't set up install mounts: no fstab loaded";
     return -1;
   }
-  for (int i = 0; i < fstab->num_entries; ++i) {
-    const Volume* v = fstab->recs + i;
-
+  for (const FstabEntry& entry : fstab) {
     // We don't want to do anything with "/".
-    if (strcmp(v->mount_point, "/") == 0) {
+    if (entry.mount_point == "/") {
       continue;
     }
 
-    if (strcmp(v->mount_point, "/tmp") == 0 || strcmp(v->mount_point, "/cache") == 0) {
-      if (ensure_path_mounted(v->mount_point) != 0) {
-        LOG(ERROR) << "Failed to mount " << v->mount_point;
+    if (entry.mount_point == "/tmp" || entry.mount_point == "/cache") {
+      if (ensure_path_mounted(entry.mount_point) != 0) {
+        LOG(ERROR) << "Failed to mount " << entry.mount_point;
         return -1;
       }
     } else {
-      if (ensure_path_unmounted(v->mount_point) != 0) {
-        LOG(ERROR) << "Failed to unmount " << v->mount_point;
+      if (ensure_path_unmounted(entry.mount_point) != 0) {
+        LOG(ERROR) << "Failed to unmount " << entry.mount_point;
         return -1;
       }
     }
   }
   return 0;
 }
+
+bool logical_partitions_mapped() {
+  return android::fs_mgr::LogicalPartitionsMapped();
+}
+
+std::string get_system_root() {
+  return android::fs_mgr::GetSystemRoot();
+}
diff --git a/roots.h b/roots.h
index 46bb77e..7b031a1 100644
--- a/roots.h
+++ b/roots.h
@@ -19,7 +19,9 @@
 
 #include <string>
 
-typedef struct fstab_rec Volume;
+#include <fstab/fstab.h>
+
+using Volume = android::fs_mgr::FstabEntry;
 
 // Load and parse volume data from /etc/recovery.fstab.
 void load_volume_table();
@@ -29,28 +31,32 @@
 
 // Make sure that the volume 'path' is on is mounted.  Returns 0 on
 // success (volume is mounted).
-int ensure_path_mounted(const char* path);
+int ensure_path_mounted(const std::string& path);
 
 // Similar to ensure_path_mounted, but allows one to specify the mount_point.
-int ensure_path_mounted_at(const char* path, const char* mount_point);
+int ensure_path_mounted_at(const std::string& path, const std::string& mount_point);
 
 // Make sure that the volume 'path' is on is unmounted.  Returns 0 on
 // success (volume is unmounted);
-int ensure_path_unmounted(const char* path);
+int ensure_path_unmounted(const std::string& path);
 
 // Reformat the given volume (must be the mount point only, eg
 // "/cache"), no paths permitted.  Attempts to unmount the volume if
 // it is mounted.
-int format_volume(const char* volume);
+int format_volume(const std::string& volume);
 
 // Reformat the given volume (must be the mount point only, eg
 // "/cache"), no paths permitted.  Attempts to unmount the volume if
 // it is mounted.
 // Copies 'directory' to root of the newly formatted volume
-int format_volume(const char* volume, const char* directory);
+int format_volume(const std::string& volume, const std::string& directory);
 
 // Ensure that all and only the volumes that packages expect to find
 // mounted (/tmp and /cache) are mounted.  Returns 0 on success.
 int setup_install_mounts();
 
+bool logical_partitions_mapped();
+
+std::string get_system_root();
+
 #endif  // RECOVERY_ROOTS_H_
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..6c00a22 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,12 +54,268 @@
   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),
-      currentIcon(NONE),
+Menu::Menu(size_t initial_selection, const DrawInterface& draw_func)
+    : selection_(initial_selection), draw_funcs_(draw_func) {}
+
+size_t Menu::selection() const {
+  return selection_;
+}
+
+TextMenu::TextMenu(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, int char_height, const DrawInterface& draw_funcs)
+    : Menu(initial_selection, draw_funcs),
+      scrollable_(scrollable),
+      max_display_items_(max_items),
+      max_item_length_(max_length),
+      text_headers_(headers),
+      menu_start_(0),
+      char_height_(char_height) {
+  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>& TextMenu::text_headers() const {
+  return text_headers_;
+}
+
+std::string TextMenu::TextItem(size_t index) const {
+  CHECK_LT(index, text_items_.size());
+
+  return text_items_[index];
+}
+
+size_t TextMenu::MenuStart() const {
+  return menu_start_;
+}
+
+size_t TextMenu::MenuEnd() const {
+  return std::min(ItemsCount(), menu_start_ + max_display_items_);
+}
+
+size_t TextMenu::ItemsCount() const {
+  return text_items_.size();
+}
+
+bool TextMenu::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 TextMenu::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_;
+}
+
+int TextMenu::DrawHeader(int x, int y) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::HEADER);
+  if (!scrollable()) {
+    offset += draw_funcs_.DrawWrappedTextLines(x, y + offset, text_headers());
+  } else {
+    offset += draw_funcs_.DrawTextLines(x, y + offset, 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 (ItemsOverflow(&cur_selection_str)) {
+      offset += draw_funcs_.DrawTextLine(x, y + offset, cur_selection_str, true);
+    }
+  }
+
+  return offset;
+}
+
+int TextMenu::DrawItems(int x, int y, int screen_width, bool long_press) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::MENU);
+  // Do not draw the horizontal rule for wear devices.
+  if (!scrollable()) {
+    offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4;
+  }
+  for (size_t i = MenuStart(); i < MenuEnd(); ++i) {
+    bool bold = false;
+    if (i == selection()) {
+      // Draw the highlight bar.
+      draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG);
+
+      int bar_height = char_height_ + 4;
+      draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height);
+
+      // Bold white text for the selected item.
+      draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
+      bold = true;
+    }
+    offset += draw_funcs_.DrawTextLine(x, y + offset, TextItem(i), bold);
+
+    draw_funcs_.SetColor(UIElement::MENU);
+  }
+  offset += draw_funcs_.DrawHorizontalRule(y + offset);
+
+  return offset;
+}
+
+GraphicMenu::GraphicMenu(const GRSurface* graphic_headers,
+                         const std::vector<const GRSurface*>& graphic_items,
+                         size_t initial_selection, const DrawInterface& draw_funcs)
+    : Menu(initial_selection, draw_funcs) {
+  graphic_headers_ = graphic_headers->Clone();
+  graphic_items_.reserve(graphic_items.size());
+  for (const auto& item : graphic_items) {
+    graphic_items_.emplace_back(item->Clone());
+  }
+}
+
+int GraphicMenu::Select(int sel) {
+  CHECK_LE(graphic_items_.size(), static_cast<size_t>(std::numeric_limits<int>::max()));
+  int count = graphic_items_.size();
+
+  // Wraps the selection at boundary if the menu is not scrollable.
+  if (sel < 0) {
+    selection_ = count - 1;
+  } else if (sel >= count) {
+    selection_ = 0;
+  } else {
+    selection_ = sel;
+  }
+
+  return selection_;
+}
+
+int GraphicMenu::DrawHeader(int x, int y) const {
+  draw_funcs_.SetColor(UIElement::HEADER);
+  draw_funcs_.DrawTextIcon(x, y, graphic_headers_.get());
+  return graphic_headers_->height;
+}
+
+int GraphicMenu::DrawItems(int x, int y, int screen_width, bool long_press) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::MENU);
+  offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4;
+
+  for (size_t i = 0; i < graphic_items_.size(); i++) {
+    auto& item = graphic_items_[i];
+    if (i == selection_) {
+      draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG);
+
+      int bar_height = item->height + 4;
+      draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height);
+
+      // Bold white text for the selected item.
+      draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
+    }
+    draw_funcs_.DrawTextIcon(x, y + offset, item.get());
+    offset += item->height;
+
+    draw_funcs_.SetColor(UIElement::MENU);
+  }
+  offset += draw_funcs_.DrawHorizontalRule(y + offset);
+
+  return offset;
+}
+
+bool GraphicMenu::Validate(size_t max_width, size_t max_height, const GRSurface* graphic_headers,
+                           const std::vector<const GRSurface*>& graphic_items) {
+  int offset = 0;
+  if (!ValidateGraphicSurface(max_width, max_height, offset, graphic_headers)) {
+    return false;
+  }
+  offset += graphic_headers->height;
+
+  for (const auto& item : graphic_items) {
+    if (!ValidateGraphicSurface(max_width, max_height, offset, item)) {
+      return false;
+    }
+    offset += item->height;
+  }
+
+  return true;
+}
+
+bool GraphicMenu::ValidateGraphicSurface(size_t max_width, size_t max_height, int y,
+                                         const GRSurface* surface) {
+  if (!surface) {
+    fprintf(stderr, "Graphic surface can not be null\n");
+    return false;
+  }
+
+  if (surface->pixel_bytes != 1 || surface->width != surface->row_bytes) {
+    fprintf(stderr, "Invalid graphic surface, pixel bytes: %zu, width: %zu row_bytes: %zu\n",
+            surface->pixel_bytes, surface->width, surface->row_bytes);
+    return false;
+  }
+
+  if (surface->width > max_width || surface->height > max_height - y) {
+    fprintf(stderr,
+            "Graphic surface doesn't fit into the screen. width: %zu, height: %zu, max_width: %zu,"
+            " max_height: %zu, vertical offset: %d\n",
+            surface->width, surface->height, max_width, max_height, y);
+    return false;
+  }
+
+  return true;
+}
+
+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),
+      current_icon_(NONE),
+      current_frame_(0),
+      intro_done_(false),
       progressBarType(EMPTY),
       progressScopeStart(0),
       progressScopeSize(0),
@@ -71,45 +328,46 @@
       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),
-      current_frame(0),
-      intro_done(false),
       stage(-1),
       max_stage(-1),
       locale_(""),
-      rtl_locale_(false),
-      updateMutex(PTHREAD_MUTEX_INITIALIZER) {}
+      rtl_locale_(false) {}
 
-GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
-  if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
-    return intro_done ? loopFrames[current_frame] : introFrames[current_frame];
+ScreenRecoveryUI::~ScreenRecoveryUI() {
+  progress_thread_stopped_ = true;
+  if (progress_thread_.joinable()) {
+    progress_thread_.join();
   }
-  return error_icon;
+  // No-op if gr_init() (via Init()) was not called or had failed.
+  gr_exit();
 }
 
-GRSurface* ScreenRecoveryUI::GetCurrentText() const {
-  switch (currentIcon) {
+const GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
+  if (current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) {
+    return intro_done_ ? loop_frames_[current_frame_].get() : intro_frames_[current_frame_].get();
+  }
+  return error_icon_.get();
+}
+
+const GRSurface* ScreenRecoveryUI::GetCurrentText() const {
+  switch (current_icon_) {
     case ERASING:
-      return erasing_text;
+      return erasing_text_.get();
     case ERROR:
-      return error_text;
+      return error_text_.get();
     case INSTALLING_UPDATE:
-      return installing_text;
+      return installing_text_.get();
     case NO_COMMAND:
-      return no_command_text;
+      return no_command_text_.get();
     case NONE:
       abort();
   }
 }
 
 int ScreenRecoveryUI::PixelsFromDp(int dp) const {
-  return dp * kDensity;
+  return dp * density_;
 }
 
 // Here's the intended layout:
@@ -137,20 +395,21 @@
 };
 
 int ScreenRecoveryUI::GetAnimationBaseline() const {
-  return GetTextBaseline() - PixelsFromDp(kLayouts[layout_][ICON]) - gr_get_height(loopFrames[0]);
+  return GetTextBaseline() - PixelsFromDp(kLayouts[layout_][ICON]) -
+         gr_get_height(loop_frames_[0].get());
 }
 
 int ScreenRecoveryUI::GetTextBaseline() const {
   return GetProgressBaseline() - PixelsFromDp(kLayouts[layout_][TEXT]) -
-         gr_get_height(installing_text);
+         gr_get_height(installing_text_.get());
 }
 
 int ScreenRecoveryUI::GetProgressBaseline() const {
-  int elements_sum = gr_get_height(loopFrames[0]) + PixelsFromDp(kLayouts[layout_][ICON]) +
-                     gr_get_height(installing_text) + PixelsFromDp(kLayouts[layout_][TEXT]) +
-                     gr_get_height(progressBarFill);
+  int elements_sum = gr_get_height(loop_frames_[0].get()) + PixelsFromDp(kLayouts[layout_][ICON]) +
+                     gr_get_height(installing_text_.get()) + PixelsFromDp(kLayouts[layout_][TEXT]) +
+                     gr_get_height(progress_bar_fill_.get());
   int bottom_gap = (ScreenHeight() - elements_sum) / 2;
-  return ScreenHeight() - bottom_gap - gr_get_height(progressBarFill);
+  return ScreenHeight() - bottom_gap - gr_get_height(progress_bar_fill_.get());
 }
 
 // Clear the screen and draw the currently selected background icon (if any).
@@ -159,20 +418,20 @@
   pagesIdentical = false;
   gr_color(0, 0, 0, 255);
   gr_clear();
-  if (currentIcon != NONE) {
+  if (current_icon_ != NONE) {
     if (max_stage != -1) {
-      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 stage_height = gr_get_height(stage_marker_empty_.get());
+      int stage_width = gr_get_width(stage_marker_empty_.get());
+      int x = (ScreenWidth() - max_stage * gr_get_width(stage_marker_empty_.get())) / 2;
+      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);
+        const auto& stage_surface = (i < stage) ? stage_marker_fill_ : stage_marker_empty_;
+        DrawSurface(stage_surface.get(), 0, 0, stage_width, stage_height, x, y);
         x += stage_width;
       }
     }
 
-    GRSurface* text_surface = GetCurrentText();
+    const auto& text_surface = GetCurrentText();
     int text_x = (ScreenWidth() - gr_get_width(text_surface)) / 2;
     int text_y = GetTextBaseline();
     gr_color(255, 255, 255, 255);
@@ -183,8 +442,8 @@
 // Draws the animation and progress bar (if any) on the screen. Does not flip pages. Should only be
 // called with updateMutex locked.
 void ScreenRecoveryUI::draw_foreground_locked() {
-  if (currentIcon != NONE) {
-    GRSurface* frame = GetCurrentFrame();
+  if (current_icon_ != NONE) {
+    const auto& frame = GetCurrentFrame();
     int frame_width = gr_get_width(frame);
     int frame_height = gr_get_height(frame);
     int frame_x = (ScreenWidth() - frame_width) / 2;
@@ -193,8 +452,8 @@
   }
 
   if (progressBarType != EMPTY) {
-    int width = gr_get_width(progressBarEmpty);
-    int height = gr_get_height(progressBarEmpty);
+    int width = gr_get_width(progress_bar_empty_.get());
+    int height = gr_get_height(progress_bar_empty_.get());
 
     int progress_x = (ScreenWidth() - width) / 2;
     int progress_y = GetProgressBaseline();
@@ -210,19 +469,20 @@
       if (rtl_locale_) {
         // Fill the progress bar from right to left.
         if (pos > 0) {
-          DrawSurface(progressBarFill, width - pos, 0, pos, height, progress_x + width - pos,
-                      progress_y);
+          DrawSurface(progress_bar_fill_.get(), width - pos, 0, pos, height,
+                      progress_x + width - pos, progress_y);
         }
         if (pos < width - 1) {
-          DrawSurface(progressBarEmpty, 0, 0, width - pos, height, progress_x, progress_y);
+          DrawSurface(progress_bar_empty_.get(), 0, 0, width - pos, height, progress_x, progress_y);
         }
       } else {
         // Fill the progress bar from left to right.
         if (pos > 0) {
-          DrawSurface(progressBarFill, 0, 0, pos, height, progress_x, progress_y);
+          DrawSurface(progress_bar_fill_.get(), 0, 0, pos, height, progress_x, progress_y);
         }
         if (pos < width - 1) {
-          DrawSurface(progressBarEmpty, pos, 0, width - pos, height, progress_x + pos, progress_y);
+          DrawSurface(progress_bar_empty_.get(), pos, 0, width - pos, height, progress_x + pos,
+                      progress_y);
         }
       }
     }
@@ -231,26 +491,26 @@
 
 void ScreenRecoveryUI::SetColor(UIElement e) const {
   switch (e) {
-    case INFO:
+    case UIElement::INFO:
       gr_color(249, 194, 0, 255);
       break;
-    case HEADER:
+    case UIElement::HEADER:
       gr_color(247, 0, 6, 255);
       break;
-    case MENU:
-    case MENU_SEL_BG:
+    case UIElement::MENU:
+    case UIElement::MENU_SEL_BG:
       gr_color(0, 106, 157, 255);
       break;
-    case MENU_SEL_BG_ACTIVE:
+    case UIElement::MENU_SEL_BG_ACTIVE:
       gr_color(0, 156, 100, 255);
       break;
-    case MENU_SEL_FG:
+    case UIElement::MENU_SEL_FG:
       gr_color(255, 255, 255, 255);
       break;
-    case LOG:
+    case UIElement::LOG:
       gr_color(196, 196, 196, 255);
       break;
-    case TEXT_FILL:
+    case UIElement::TEXT_FILL:
       gr_color(0, 0, 0, 160);
       break;
     default:
@@ -264,62 +524,65 @@
   SetLocale(locales_entries[sel]);
   std::vector<std::string> text_name = { "erasing_text", "error_text", "installing_text",
                                          "installing_security_text", "no_command_text" };
-  std::unordered_map<std::string, std::unique_ptr<GRSurface, decltype(&free)>> surfaces;
+  std::unordered_map<std::string, std::unique_ptr<GRSurface>> surfaces;
   for (const auto& name : text_name) {
-    GRSurface* text_image = nullptr;
-    LoadLocalizedBitmap(name.c_str(), &text_image);
+    auto text_image = LoadLocalizedBitmap(name);
     if (!text_image) {
       Print("Failed to load %s\n", name.c_str());
       return;
     }
-    surfaces.emplace(name, std::unique_ptr<GRSurface, decltype(&free)>(text_image, &free));
+    surfaces.emplace(name, std::move(text_image));
   }
 
-  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);
+  SetColor(UIElement::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);
+    SetColor(UIElement::LOG);
+    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) {
@@ -342,7 +605,7 @@
   return gr_fb_height();
 }
 
-void ScreenRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
+void ScreenRecoveryUI::DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx,
                                    int dy) const {
   gr_blit(surface, sx, sy, w, h, dx, dy);
 }
@@ -360,61 +623,55 @@
   gr_fill(x, y, w, h);
 }
 
-void ScreenRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
+void ScreenRecoveryUI::DrawTextIcon(int x, int y, const GRSurface* surface) const {
   gr_texticon(x, y, surface);
 }
 
-int ScreenRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
-  gr_text(gr_sys_font(), x, y, line, bold);
+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,50 +685,45 @@
   gr_color(0, 0, 0, 255);
   gr_clear();
 
-  int y = kMarginHeight;
-  if (show_menu) {
-    static constexpr int kMenuIndent = 4;
-    int x = kMarginWidth + kMenuIndent;
+  // 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);
+}
 
-    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);
+// 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_) {
+    int x = margin_width_ + kMenuIndent;
+
+    SetColor(UIElement::INFO);
+
+    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);
 
-    SetColor(HEADER);
-    // Ignore kMenuIndent, which is not taken into account by text_cols_.
-    y += DrawWrappedTextLines(kMarginWidth, y, menu_headers_);
+    y += DrawTextLines(x, y, help_message);
 
-    SetColor(MENU);
-    y += DrawHorizontalRule(y) + 4;
-    for (int i = 0; i < menu_items; ++i) {
-      if (i == menu_sel) {
-        // Draw the highlight bar.
-        SetColor(IsLongPress() ? MENU_SEL_BG_ACTIVE : MENU_SEL_BG);
-        DrawHighlightBar(0, y - 2, ScreenWidth(), char_height_ + 4);
-        // Bold white text for the selected item.
-        SetColor(MENU_SEL_FG);
-        y += DrawTextLine(x, y, menu_[i].c_str(), true);
-        SetColor(MENU);
-      } else {
-        y += DrawTextLine(x, y, menu_[i].c_str(), false);
-      }
-    }
-    y += DrawHorizontalRule(y);
+    y += menu_->DrawHeader(x, y);
+    y += menu_->DrawItems(x, y, ScreenWidth(), IsLongPress());
   }
 
   // Display from the bottom up, until we hit the top of the screen, the bottom of the menu, or
   // we've displayed the entire text buffer.
-  SetColor(LOG);
+  SetColor(UIElement::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 +748,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 ((current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) && !show_text) {
+        if (!intro_done_) {
+          if (current_frame_ == intro_frames_.size() - 1) {
+            intro_done_ = true;
+            current_frame_ = 0;
+          } else {
+            ++current_frame_;
+          }
         } else {
-          ++current_frame;
+          current_frame_ = (current_frame_ + 1) % loop_frames_.size();
         }
-      } 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);
@@ -550,18 +796,23 @@
   }
 }
 
-void ScreenRecoveryUI::LoadBitmap(const char* filename, GRSurface** surface) {
-  int result = res_create_display_surface(filename, surface);
-  if (result < 0) {
-    LOG(ERROR) << "couldn't load bitmap " << filename << " (error " << result << ")";
+std::unique_ptr<GRSurface> ScreenRecoveryUI::LoadBitmap(const std::string& filename) {
+  GRSurface* surface;
+  if (auto result = res_create_display_surface(filename.c_str(), &surface); result < 0) {
+    LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")";
+    return nullptr;
   }
+  return std::unique_ptr<GRSurface>(surface);
 }
 
-void ScreenRecoveryUI::LoadLocalizedBitmap(const char* filename, GRSurface** surface) {
-  int result = res_create_localized_alpha_surface(filename, locale_.c_str(), surface);
-  if (result < 0) {
-    LOG(ERROR) << "couldn't load bitmap " << filename << " (error " << result << ")";
+std::unique_ptr<GRSurface> ScreenRecoveryUI::LoadLocalizedBitmap(const std::string& filename) {
+  GRSurface* surface;
+  if (auto result = res_create_localized_alpha_surface(filename.c_str(), locale_.c_str(), &surface);
+      result < 0) {
+    LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")";
+    return nullptr;
   }
+  return std::unique_ptr<GRSurface>(surface);
 }
 
 static char** Alloc2d(size_t rows, size_t cols) {
@@ -576,27 +827,41 @@
 // Choose the right background string to display during update.
 void ScreenRecoveryUI::SetSystemUpdateText(bool security_update) {
   if (security_update) {
-    LoadLocalizedBitmap("installing_security_text", &installing_text);
+    installing_text_ = LoadLocalizedBitmap("installing_security_text");
   } else {
-    LoadLocalizedBitmap("installing_text", &installing_text);
+    installing_text_ = LoadLocalizedBitmap("installing_text");
   }
   Redraw();
 }
 
 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::LoadWipeDataMenuText() {
+  // Ignores the errors since the member variables will stay as nullptr.
+  cancel_wipe_data_text_ = LoadLocalizedBitmap("cancel_wipe_data_text");
+  factory_data_reset_text_ = LoadLocalizedBitmap("factory_data_reset_text");
+  try_again_text_ = LoadLocalizedBitmap("try_again_text");
+  wipe_data_confirmation_text_ = LoadLocalizedBitmap("wipe_data_confirmation_text");
+  wipe_data_menu_header_text_ = LoadLocalizedBitmap("wipe_data_menu_header_text");
   return true;
 }
 
 bool ScreenRecoveryUI::Init(const std::string& locale) {
   RecoveryUI::Init(locale);
 
+  if (gr_init() == -1) {
+    return false;
+  }
+
   if (!InitTextParams()) {
     return false;
   }
@@ -614,31 +879,38 @@
   // Set up the locale info.
   SetLocale(locale);
 
-  LoadBitmap("icon_error", &error_icon);
+  error_icon_ = LoadBitmap("icon_error");
 
-  LoadBitmap("progress_empty", &progressBarEmpty);
-  LoadBitmap("progress_fill", &progressBarFill);
+  progress_bar_empty_ = LoadBitmap("progress_empty");
+  progress_bar_fill_ = LoadBitmap("progress_fill");
+  stage_marker_empty_ = LoadBitmap("stage_empty");
+  stage_marker_fill_ = LoadBitmap("stage_fill");
 
-  LoadBitmap("stage_empty", &stageMarkerEmpty);
-  LoadBitmap("stage_fill", &stageMarkerFill);
+  erasing_text_ = LoadLocalizedBitmap("erasing_text");
+  no_command_text_ = LoadLocalizedBitmap("no_command_text");
+  error_text_ = LoadLocalizedBitmap("error_text");
 
-  // Background text for "installing_update" could be "installing update"
-  // or "installing security update". It will be set after UI init according
-  // to commands in BCB.
-  installing_text = nullptr;
-  LoadLocalizedBitmap("erasing_text", &erasing_text);
-  LoadLocalizedBitmap("no_command_text", &no_command_text);
-  LoadLocalizedBitmap("error_text", &error_text);
+  // Background text for "installing_update" could be "installing update" or
+  // "installing security update". It will be set after Init() according to the commands in BCB.
+  installing_text_.reset();
+
+  LoadWipeDataMenuText();
 
   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;
@@ -652,39 +924,39 @@
     }
   }
 
-  intro_frames = intro_frame_names.size();
-  loop_frames = loop_frame_names.size();
+  size_t intro_frames = intro_frame_names.size();
+  size_t loop_frames = loop_frame_names.size();
 
   // It's okay to not have an intro.
-  if (intro_frames == 0) intro_done = true;
+  if (intro_frames == 0) intro_done_ = true;
   // But you must have an animation.
   if (loop_frames == 0) abort();
 
   std::sort(intro_frame_names.begin(), intro_frame_names.end());
   std::sort(loop_frame_names.begin(), loop_frame_names.end());
 
-  introFrames = new GRSurface*[intro_frames];
-  for (size_t i = 0; i < intro_frames; i++) {
-    LoadBitmap(intro_frame_names.at(i).c_str(), &introFrames[i]);
+  intro_frames_.clear();
+  intro_frames_.reserve(intro_frames);
+  for (const auto& frame_name : intro_frame_names) {
+    intro_frames_.emplace_back(LoadBitmap(frame_name));
   }
 
-  loopFrames = new GRSurface*[loop_frames];
-  for (size_t i = 0; i < loop_frames; i++) {
-    LoadBitmap(loop_frame_names.at(i).c_str(), &loopFrames[i]);
+  loop_frames_.clear();
+  loop_frames_.reserve(loop_frames);
+  for (const auto& frame_name : loop_frame_names) {
+    loop_frames_.emplace_back(LoadBitmap(frame_name));
   }
 }
 
 void ScreenRecoveryUI::SetBackground(Icon icon) {
-  pthread_mutex_lock(&updateMutex);
+  std::lock_guard<std::mutex> lg(updateMutex);
 
-  currentIcon = icon;
+  current_icon_ = 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 +964,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,30 +975,27 @@
   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) {
     // Skip updates that aren't visibly different.
-    int width = gr_get_width(progressBarEmpty);
+    int width = gr_get_width(progress_bar_empty_.get());
     float scale = width * progressScopeSize;
     if ((int)(progress * scale) != (int)(fraction * scale)) {
       progress = fraction;
       update_progress_locked();
     }
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::SetStage(int current, int max) {
-  pthread_mutex_lock(&updateMutex);
+  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 +1006,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 +1019,6 @@
     text_[text_row_][text_col_] = '\0';
     update_screen_locked();
   }
-  pthread_mutex_unlock(&updateMutex);
 }
 
 void ScreenRecoveryUI::Print(const char* fmt, ...) {
@@ -769,23 +1036,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 +1071,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 +1104,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 +1119,178 @@
   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;
-    update_screen_locked();
+std::unique_ptr<Menu> ScreenRecoveryUI::CreateMenu(
+    const GRSurface* graphic_header, const std::vector<const GRSurface*>& graphic_items,
+    const std::vector<std::string>& text_headers, const std::vector<std::string>& text_items,
+    size_t initial_selection) const {
+  // horizontal unusable area: margin width + menu indent
+  size_t max_width = ScreenWidth() - margin_width_ - kMenuIndent;
+  // vertical unusable area: margin height + title lines + helper message + high light bar.
+  // It is safe to reserve more space.
+  size_t max_height = ScreenHeight() - margin_height_ - char_height_ * (title_lines_.size() + 3);
+  if (GraphicMenu::Validate(max_width, max_height, graphic_header, graphic_items)) {
+    return std::make_unique<GraphicMenu>(graphic_header, graphic_items, initial_selection, *this);
   }
-  pthread_mutex_unlock(&updateMutex);
+
+  fprintf(stderr, "Failed to initialize graphic menu, falling back to use the text menu.\n");
+
+  return CreateMenu(text_headers, text_items, initial_selection);
+}
+
+std::unique_ptr<Menu> ScreenRecoveryUI::CreateMenu(const std::vector<std::string>& text_headers,
+                                                   const std::vector<std::string>& text_items,
+                                                   size_t initial_selection) const {
+  if (text_rows_ > 0 && text_cols_ > 1) {
+    return std::make_unique<TextMenu>(scrollable_menu_, text_rows_, text_cols_ - 1, text_headers,
+                                      text_items, initial_selection, char_height_, *this);
+  }
+
+  fprintf(stderr, "Failed to create text menu, text_rows %zu, text_cols %zu.\n", text_rows_,
+          text_cols_);
+  return nullptr;
 }
 
 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;
-    update_screen_locked();
+size_t ScreenRecoveryUI::ShowMenu(std::unique_ptr<Menu>&& menu, 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);
+
+  CHECK(menu != nullptr);
+
+  // Starts and displays the menu
+  menu_ = std::move(menu);
+  Redraw();
+
+  int selected = menu_->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.";
+        menu_.reset();
+        Redraw();
+        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;
+    }
   }
-  pthread_mutex_unlock(&updateMutex);
+
+  menu_.reset();
+  Redraw();
+
+  return chosen_item;
+}
+
+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) {
+  auto menu = CreateMenu(headers, items, initial_selection);
+  if (menu == nullptr) {
+    return initial_selection;
+  }
+
+  return ShowMenu(CreateMenu(headers, items, initial_selection), menu_only, key_handler);
+}
+
+size_t ScreenRecoveryUI::ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers,
+                                                const std::vector<std::string>& backup_items,
+                                                const std::function<int(int, bool)>& key_handler) {
+  auto wipe_data_menu = CreateMenu(wipe_data_menu_header_text_.get(),
+                                   { try_again_text_.get(), factory_data_reset_text_.get() },
+                                   backup_headers, backup_items, 0);
+  if (wipe_data_menu == nullptr) {
+    return 0;
+  }
+
+  return ShowMenu(std::move(wipe_data_menu), true, key_handler);
+}
+
+size_t ScreenRecoveryUI::ShowPromptWipeDataConfirmationMenu(
+    const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items,
+    const std::function<int(int, bool)>& key_handler) {
+  auto confirmation_menu =
+      CreateMenu(wipe_data_confirmation_text_.get(),
+                 { cancel_wipe_data_text_.get(), factory_data_reset_text_.get() }, backup_headers,
+                 backup_items, 0);
+  if (confirmation_menu == nullptr) {
+    return 0;
+  }
+
+  return ShowMenu(std::move(confirmation_menu), true, key_handler);
 }
 
 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 +1304,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..acd44c8 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -17,35 +17,188 @@
 #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"
 
 // From minui/minui.h.
-struct GRSurface;
+class GRSurface;
+
+enum class UIElement {
+  HEADER,
+  MENU,
+  MENU_SEL_BG,
+  MENU_SEL_BG_ACTIVE,
+  MENU_SEL_FG,
+  LOG,
+  TEXT_FILL,
+  INFO
+};
+
+// Interface to draw the UI elements on the screen.
+class DrawInterface {
+ public:
+  virtual ~DrawInterface() = default;
+
+  // Sets the color to the predefined value for |element|.
+  virtual void SetColor(UIElement element) const = 0;
+
+  // Draws a highlight bar at (x, y) - (x + width, y + height).
+  virtual void DrawHighlightBar(int x, int y, int width, int height) const = 0;
+
+  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
+  virtual int DrawHorizontalRule(int y) const = 0;
+
+  // Draws a line of text. Returns the offset it should be moving along Y-axis.
+  virtual int DrawTextLine(int x, int y, const std::string& line, bool bold) const = 0;
+
+  // Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
+  virtual void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                           int dy) const = 0;
+
+  // Draws rectangle at (x, y) - (x + w, y + h).
+  virtual void DrawFill(int x, int y, int w, int h) const = 0;
+
+  // Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
+  virtual void DrawTextIcon(int x, int y, const GRSurface* surface) const = 0;
+
+  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
+  virtual int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const = 0;
+
+  // 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.
+  virtual int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const = 0;
+};
+
+// Interface for classes that maintain the menu selection and display.
+class Menu {
+ public:
+  virtual ~Menu() = default;
+  // Returns the current menu selection.
+  size_t selection() const;
+  // Sets the current selection to |sel|. Handle the overflow cases depending on if the menu is
+  // scrollable.
+  virtual int Select(int sel) = 0;
+  // Displays the menu headers on the screen at offset x, y
+  virtual int DrawHeader(int x, int y) const = 0;
+  // Iterates over the menu items and displays each of them at offset x, y.
+  virtual int DrawItems(int x, int y, int screen_width, bool long_press) const = 0;
+
+ protected:
+  Menu(size_t initial_selection, const DrawInterface& draw_func);
+  // Current menu selection.
+  size_t selection_;
+  // Reference to the class that implements all the draw functions.
+  const DrawInterface& draw_funcs_;
+};
+
+// This class uses strings as the menu header and items.
+class TextMenu : public Menu {
+ public:
+  // Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial
+  // selection to |initial_selection|.
+  TextMenu(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, int char_height, const DrawInterface& draw_funcs);
+
+  int Select(int sel) override;
+  int DrawHeader(int x, int y) const override;
+  int DrawItems(int x, int y, int screen_width, bool long_press) const override;
+
+  bool scrollable() const {
+    return scrollable_;
+  }
+
+  // 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;
+
+ 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_;
+
+  // Height in pixels of each character.
+  int char_height_;
+};
+
+// This class uses GRSurface's as the menu header and items.
+class GraphicMenu : public Menu {
+ public:
+  // Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial
+  // selection to |initial_selection|. |headers| and |items| will be made local copies.
+  GraphicMenu(const GRSurface* graphic_headers, const std::vector<const GRSurface*>& graphic_items,
+              size_t initial_selection, const DrawInterface& draw_funcs);
+
+  int Select(int sel) override;
+  int DrawHeader(int x, int y) const override;
+  int DrawItems(int x, int y, int screen_width, bool long_press) const override;
+
+  // Checks if all the header and items are valid GRSurface's; and that they can fit in the area
+  // defined by |max_width| and |max_height|.
+  static bool Validate(size_t max_width, size_t max_height, const GRSurface* graphic_headers,
+                       const std::vector<const GRSurface*>& graphic_items);
+
+  // Returns true if |surface| fits on the screen with a vertical offset |y|.
+  static bool ValidateGraphicSurface(size_t max_width, size_t max_height, int y,
+                                     const GRSurface* surface);
+
+ private:
+  // Menu headers and items in graphic icons. These are the copies owned by the class instance.
+  std::unique_ptr<GRSurface> graphic_headers_;
+  std::vector<std::unique_ptr<GRSurface>> graphic_items_;
+};
 
 // Implementation of RecoveryUI appropriate for devices with a screen
 // (shows an icon + a progress bar, text logging, menu, etc.)
-class ScreenRecoveryUI : public RecoveryUI {
+class ScreenRecoveryUI : public RecoveryUI, public DrawInterface {
  public:
-  enum UIElement {
-    HEADER,
-    MENU,
-    MENU_SEL_BG,
-    MENU_SEL_BG_ACTIVE,
-    MENU_SEL_FG,
-    LOG,
-    TEXT_FILL,
-    INFO
-  };
-
   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,48 +219,83 @@
   // 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;
 
   void Redraw();
 
-  void SetColor(UIElement e) const;
+  // 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();
 
-  // 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);
+  // Displays the localized wipe data menu.
+  size_t ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers,
+                                const std::vector<std::string>& backup_items,
+                                const std::function<int(int, bool)>& key_handler) override;
+
+  // Displays the localized wipe data confirmation menu.
+  size_t ShowPromptWipeDataConfirmationMenu(
+      const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items,
+      const std::function<int(int, bool)>& key_handler) override;
 
  protected:
+  static constexpr int kMenuIndent = 4;
+
   // 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();
 
+  virtual bool LoadWipeDataMenuText();
+
+  // Creates a GraphicMenu with |graphic_header| and |graphic_items|. If the GraphicMenu isn't
+  // valid or it doesn't fit on the screen; falls back to create a TextMenu instead. If succeeds,
+  // returns a unique pointer to the created menu; otherwise returns nullptr.
+  virtual std::unique_ptr<Menu> CreateMenu(const GRSurface* graphic_header,
+                                           const std::vector<const GRSurface*>& graphic_items,
+                                           const std::vector<std::string>& text_headers,
+                                           const std::vector<std::string>& text_items,
+                                           size_t initial_selection) const;
+
+  // Creates a TextMenu with |text_headers| and |text_items|; and sets the menu selection to
+  // |initial_selection|.
+  virtual std::unique_ptr<Menu> CreateMenu(const std::vector<std::string>& text_headers,
+                                           const std::vector<std::string>& text_items,
+                                           size_t initial_selection) const;
+
+  // Takes the ownership of |menu| and displays it.
+  virtual size_t ShowMenu(std::unique_ptr<Menu>&& menu, bool menu_only,
+                          const std::function<int(int, bool)>& key_handler);
+
+  // Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
+  // selected.
+  virtual int SelectMenu(int sel);
+
   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;
+  const GRSurface* GetCurrentFrame() const;
+  const GRSurface* GetCurrentText() const;
 
-  static void* ProgressThreadStartRoutine(void* data);
   void ProgressThreadLoop();
 
   virtual void ShowFile(FILE*);
@@ -116,8 +304,8 @@
   void ClearText();
 
   void LoadAnimation();
-  void LoadBitmap(const char* filename, GRSurface** surface);
-  void LoadLocalizedBitmap(const char* filename, GRSurface** surface);
+  std::unique_ptr<GRSurface> LoadBitmap(const std::string& filename);
+  std::unique_ptr<GRSurface> LoadLocalizedBitmap(const std::string& filename);
 
   int PixelsFromDp(int dp) const;
   virtual int GetAnimationBaseline() const;
@@ -129,43 +317,48 @@
   // Returns pixel height of draw buffer.
   virtual int ScreenHeight() const;
 
-  // Draws a highlight bar at (x, y) - (x + width, y + height).
-  virtual void DrawHighlightBar(int x, int y, int width, int height) const;
-  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
-  virtual int DrawHorizontalRule(int y) const;
-  // Draws a line of text. Returns the offset it should be moving along Y-axis.
-  virtual int DrawTextLine(int x, int y, const char* line, bool bold) const;
-  // Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
-  virtual void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const;
-  // Draws rectangle at (x, y) - (x + w, y + h).
-  virtual void DrawFill(int x, int y, int w, int h) const;
-  // Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
-  virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
-  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
-  int DrawTextLines(int x, int y, const char* const* lines) const;
-  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
-  // Returns the offset it should be moving along Y-axis.
-  int DrawWrappedTextLines(int x, int y, const char* const* lines) const;
-
-  Icon currentIcon;
+  // Implementation of the draw functions in DrawInterface.
+  void SetColor(UIElement e) const override;
+  void DrawHighlightBar(int x, int y, int width, int height) const override;
+  int DrawHorizontalRule(int y) const override;
+  void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                   int dy) const override;
+  void DrawFill(int x, int y, int w, int h) const override;
+  void DrawTextIcon(int x, int y, const GRSurface* surface) const override;
+  int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
+  int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const override;
+  int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const override;
 
   // The layout to use.
   int layout_;
 
-  GRSurface* error_icon;
+  // The images that contain localized texts.
+  std::unique_ptr<GRSurface> erasing_text_;
+  std::unique_ptr<GRSurface> error_text_;
+  std::unique_ptr<GRSurface> installing_text_;
+  std::unique_ptr<GRSurface> no_command_text_;
 
-  GRSurface* erasing_text;
-  GRSurface* error_text;
-  GRSurface* installing_text;
-  GRSurface* no_command_text;
+  // Localized text images for the wipe data menu.
+  std::unique_ptr<GRSurface> cancel_wipe_data_text_;
+  std::unique_ptr<GRSurface> factory_data_reset_text_;
+  std::unique_ptr<GRSurface> try_again_text_;
+  std::unique_ptr<GRSurface> wipe_data_confirmation_text_;
+  std::unique_ptr<GRSurface> wipe_data_menu_header_text_;
 
-  GRSurface** introFrames;
-  GRSurface** loopFrames;
+  // current_icon_ points to one of the frames in intro_frames_ or loop_frames_, indexed by
+  // current_frame_, or error_icon_.
+  Icon current_icon_;
+  std::unique_ptr<GRSurface> error_icon_;
+  std::vector<std::unique_ptr<GRSurface>> intro_frames_;
+  std::vector<std::unique_ptr<GRSurface>> loop_frames_;
+  size_t current_frame_;
+  bool intro_done_;
 
-  GRSurface* progressBarEmpty;
-  GRSurface* progressBarFill;
-  GRSurface* stageMarkerEmpty;
-  GRSurface* stageMarkerFill;
+  // progress_bar and stage_marker images.
+  std::unique_ptr<GRSurface> progress_bar_empty_;
+  std::unique_ptr<GRSurface> progress_bar_fill_;
+  std::unique_ptr<GRSurface> stage_marker_empty_;
+  std::unique_ptr<GRSurface> stage_marker_fill_;
 
   ProgressType progressBarType;
 
@@ -184,22 +377,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_;
-
-  // Number of intro frames and loop frames in the animation.
-  size_t intro_frames;
-  size_t loop_frames;
-
-  size_t current_frame;
-  bool intro_done;
+  std::thread progress_thread_;
+  std::atomic<bool> progress_thread_stopped_{ false };
 
   int stage, max_stage;
 
@@ -210,7 +397,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..fb1d8c7 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,30 @@
     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 {}
+
+  size_t ShowPromptWipeDataMenu(const std::vector<std::string>& /* backup_headers */,
+                                const std::vector<std::string>& /* backup_items */,
+                                const std::function<int(int, bool)>& /* key_handle */) override {
+    return 0;
+  }
+
+  size_t ShowPromptWipeDataConfirmationMenu(
+      const std::vector<std::string>& /* backup_headers */,
+      const std::vector<std::string>& /* backup_items */,
+      const std::function<int(int, bool)>& /* key_handle */) override {
+    return 0;
+  }
+
+  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..898ed7d
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,207 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+    name: "recovery_test_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    include_dirs: [
+        "bootable/recovery",
+    ],
+
+    shared_libs: [
+        "libbase",
+        "libcrypto",
+        "libcutils",
+        "liblog",
+        "libpng",
+        "libprocessgroup",
+        "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_noltopgo",
+    "libbinderthreadstate",
+    "liblp",
+    "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..08414b7
--- /dev/null
+++ b/tests/component/applypatch_modes_test.cpp
@@ -0,0 +1,198 @@
+/*
+ * 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 <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/bootloader_message_test.cpp b/tests/component/bootloader_message_test.cpp
index 6cc59a4..b005d19 100644
--- a/tests/component/bootloader_message_test.cpp
+++ b/tests/component/bootloader_message_test.cpp
@@ -17,8 +17,8 @@
 #include <string>
 #include <vector>
 
+#include <android-base/file.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <bootloader_message/bootloader_message.h>
 #include <gtest/gtest.h>
 
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..e76ccbd 100644
--- a/tests/component/imgdiff_test.cpp
+++ b/tests/component/imgdiff_test.cpp
@@ -25,7 +25,6 @@
 #include <android-base/memory.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <applypatch/imgdiff.h>
 #include <applypatch/imgdiff_image.h>
 #include <applypatch/imgpatch.h>
@@ -197,12 +196,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 +216,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 +249,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 +309,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 +346,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 +448,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..47a5471 100644
--- a/tests/component/install_test.cpp
+++ b/tests/component/install_test.cpp
@@ -20,30 +20,39 @@
 #include <unistd.h>
 
 #include <algorithm>
+#include <random>
 #include <string>
 #include <vector>
 
 #include <android-base/file.h>
 #include <android-base/properties.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 #include <vintf/VintfObjectRecovery.h>
 #include <ziparchive/zip_archive.h>
 #include <ziparchive/zip_writer.h>
 
 #include "install.h"
+#include "otautil/paths.h"
 #include "private/install.h"
 
+static void BuildZipArchive(const std::map<std::string, std::string>& file_map, int fd,
+                            int compression_type) {
+  FILE* zip_file = fdopen(fd, "w");
+  ZipWriter writer(zip_file);
+  for (const auto& [name, content] : file_map) {
+    ASSERT_EQ(0, writer.StartEntry(name.c_str(), compression_type));
+    ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, writer.FinishEntry());
+  }
+  ASSERT_EQ(0, writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file));
+}
+
 TEST(InstallTest, verify_package_compatibility_no_entry) {
   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));
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
 
   // Doesn't contain compatibility zip entry.
   ZipArchiveHandle zip;
@@ -54,12 +63,7 @@
 
 TEST(InstallTest, verify_package_compatibility_invalid_entry) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  ASSERT_EQ(0, writer.StartEntry("compatibility.zip", 0));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { "compatibility.zip", "" } }, temp_file.release(), kCompressStored);
 
   // Empty compatibility zip entry.
   ZipArchiveHandle zip;
@@ -70,77 +74,51 @@
 
 TEST(InstallTest, read_metadata_from_package_smoke) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored));
-  const std::string content("abcdefg");
-  ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  const std::string content("abc=defg");
+  BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file.release(),
+                  kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
-  std::string metadata;
-  ASSERT_TRUE(read_metadata_from_package(zip, &metadata));
-  ASSERT_EQ(content, metadata);
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ("defg", metadata["abc"]);
   CloseArchive(zip);
 
   TemporaryFile temp_file2;
-  FILE* zip_file2 = fdopen(temp_file2.release(), "w");
-  ZipWriter writer2(zip_file2);
-  ASSERT_EQ(0, writer2.StartEntry("META-INF/com/android/metadata", kCompressDeflated));
-  ASSERT_EQ(0, writer2.WriteBytes(content.data(), content.size()));
-  ASSERT_EQ(0, writer2.FinishEntry());
-  ASSERT_EQ(0, writer2.Finish());
-  ASSERT_EQ(0, fclose(zip_file2));
+  BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file2.release(),
+                  kCompressDeflated);
 
   ASSERT_EQ(0, OpenArchive(temp_file2.path, &zip));
   metadata.clear();
-  ASSERT_TRUE(read_metadata_from_package(zip, &metadata));
-  ASSERT_EQ(content, metadata);
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ("defg", metadata["abc"]);
   CloseArchive(zip);
 }
 
 TEST(InstallTest, read_metadata_from_package_no_entry) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  ASSERT_EQ(0, writer.StartEntry("dummy_entry", kCompressStored));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
-  std::string metadata;
-  ASSERT_FALSE(read_metadata_from_package(zip, &metadata));
+  std::map<std::string, std::string> metadata;
+  ASSERT_FALSE(ReadMetadataFromPackage(zip, &metadata));
   CloseArchive(zip);
 }
 
 TEST(InstallTest, verify_package_compatibility_with_libvintf_malformed_xml) {
   TemporaryFile compatibility_zip_file;
-  FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w");
-  ZipWriter compatibility_zip_writer(compatibility_zip);
-  ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated));
   std::string malformed_xml = "malformed";
-  ASSERT_EQ(0, compatibility_zip_writer.WriteBytes(malformed_xml.data(), malformed_xml.size()));
-  ASSERT_EQ(0, compatibility_zip_writer.FinishEntry());
-  ASSERT_EQ(0, compatibility_zip_writer.Finish());
-  ASSERT_EQ(0, fclose(compatibility_zip));
+  BuildZipArchive({ { "system_manifest.xml", malformed_xml } }, compatibility_zip_file.release(),
+                  kCompressDeflated);
 
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored));
   std::string compatibility_zip_content;
   ASSERT_TRUE(
       android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content));
-  ASSERT_EQ(0,
-            writer.WriteBytes(compatibility_zip_content.data(), compatibility_zip_content.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(),
+                  kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
@@ -165,27 +143,15 @@
   ASSERT_TRUE(
       android::base::ReadFileToString(system_manifest_xml_path, &system_manifest_xml_content));
   TemporaryFile compatibility_zip_file;
-  FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w");
-  ZipWriter compatibility_zip_writer(compatibility_zip);
-  ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated));
-  ASSERT_EQ(0, compatibility_zip_writer.WriteBytes(system_manifest_xml_content.data(),
-                                                   system_manifest_xml_content.size()));
-  ASSERT_EQ(0, compatibility_zip_writer.FinishEntry());
-  ASSERT_EQ(0, compatibility_zip_writer.Finish());
-  ASSERT_EQ(0, fclose(compatibility_zip));
+  BuildZipArchive({ { "system_manifest.xml", system_manifest_xml_content } },
+                  compatibility_zip_file.release(), kCompressDeflated);
 
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored));
   std::string compatibility_zip_content;
   ASSERT_TRUE(
       android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content));
-  ASSERT_EQ(0,
-            writer.WriteBytes(compatibility_zip_content.data(), compatibility_zip_content.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(),
+                  kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
@@ -199,72 +165,10 @@
   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);
-  ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.StartEntry("payload_properties.txt", kCompressStored));
-  const std::string properties = "some_properties";
-  ASSERT_EQ(0, writer.WriteBytes(properties.data(), properties.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  // A metadata entry is mandatory.
-  ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored));
-  std::string device = android::base::GetProperty("ro.product.device", "");
-  ASSERT_NE("", device);
-  std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
-  ASSERT_NE("", timestamp);
-
-  std::vector<std::string> meta{ "ota-type=AB", "pre-device=" + device,
-                                 "post-timestamp=" + timestamp };
-  if (!serialno.empty()) {
-    meta.push_back("serialno=" + serialno);
-  }
-  std::string metadata = android::base::Join(meta, "\n");
-  ASSERT_EQ(0, writer.WriteBytes(metadata.data(), metadata.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
-
-  ZipArchiveHandle zip;
-  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
-  ZipString payload_name("payload.bin");
-  ZipEntry payload_entry;
-  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(5U, cmd.size());
-    ASSERT_EQ(binary_path, cmd[0]);
-    ASSERT_EQ("--payload=file://" + package, cmd[1]);
-    ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]);
-    ASSERT_EQ("--headers=" + properties, cmd[3]);
-    ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
-  } else {
-    ASSERT_EQ(INSTALL_ERROR, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
-  }
-  CloseArchive(zip);
-}
-#endif  // AB_OTA_UPDATER
-
-TEST(InstallTest, update_binary_command_smoke) {
-#ifdef AB_OTA_UPDATER
-  // Empty serialno will pass the verification.
-  VerifyAbUpdateBinaryCommand({});
-#else
-  TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.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));
+  BuildZipArchive({ { UPDATE_BINARY_NAME, "" } }, temp_file.release(), kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
@@ -272,8 +176,9 @@
   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, update_binary_command(package, zip, binary_path, 0, status_fd, &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
@@ -285,7 +190,7 @@
 
   // 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(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
@@ -297,19 +202,81 @@
   ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
 
   CloseArchive(zip);
-#endif  // AB_OTA_UPDATER
 }
 
-TEST(InstallTest, update_binary_command_invalid) {
-#ifdef AB_OTA_UPDATER
+TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  // Missing payload_properties.txt.
-  ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored));
-  ASSERT_EQ(0, writer.FinishEntry());
-  // A metadata entry is mandatory.
-  ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored));
+  // The archive must have something to be opened correctly.
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
+
+  // 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;
+
+  const std::string properties = "some_properties";
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+  std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
+  ASSERT_NE("", timestamp);
+
+  std::vector<std::string> meta{ "ota-type=AB", "pre-device=" + device,
+                                 "post-timestamp=" + timestamp };
+  if (!serialno.empty()) {
+    meta.push_back("serialno=" + serialno);
+  }
+  std::string metadata_string = android::base::Join(meta, "\n");
+
+  BuildZipArchive({ { "payload.bin", "" },
+                    { "payload_properties.txt", properties },
+                    { "META-INF/com/android/metadata", metadata_string } },
+                  temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+  ZipString payload_name("payload.bin");
+  ZipEntry payload_entry;
+  ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry));
+
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  if (success) {
+    ASSERT_EQ(0, CheckPackageMetadata(metadata, OtaType::AB));
+
+    int status_fd = 10;
+    std::string package = "/path/to/update.zip";
+    std::vector<std::string> cmd;
+    ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
+    ASSERT_EQ(5U, cmd.size());
+    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, CheckPackageMetadata(metadata, OtaType::AB));
+  }
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, SetUpAbUpdateCommands) {
+  // Empty serialno will pass the verification.
+  VerifyAbUpdateCommands({});
+}
+
+TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) {
+  TemporaryFile temp_file;
+
   std::string device = android::base::GetProperty("ro.product.device", "");
   ASSERT_NE("", device);
   std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
@@ -319,69 +286,286 @@
           "ota-type=AB", "pre-device=" + device, "post-timestamp=" + timestamp,
       },
       "\n");
-  ASSERT_EQ(0, writer.WriteBytes(metadata.data(), metadata.size()));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+
+  BuildZipArchive(
+      {
+          { "payload.bin", "" },
+          { "META-INF/com/android/metadata", metadata },
+      },
+      temp_file.release(), kCompressStored);
 
   ZipArchiveHandle zip;
   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
+
+static void test_check_package_metadata(const std::string& metadata_string, OtaType ota_type,
+                                        int exptected_result) {
+  TemporaryFile temp_file;
+  BuildZipArchive(
+      {
+          { "META-INF/com/android/metadata", metadata_string },
+      },
+      temp_file.release(), kCompressStored);
+
+  ZipArchiveHandle zip;
+  ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+
+  std::map<std::string, std::string> metadata;
+  ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata));
+  ASSERT_EQ(exptected_result, CheckPackageMetadata(metadata, ota_type));
+  CloseArchive(zip);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ota_type) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // ota-type must be present
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "pre-device=" + device,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // Checks if ota-type matches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_device_type) {
+  // device type can not be empty
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  // device type mismatches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=dummy_device_type",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_serial_number_smoke) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // Serial number doesn't need to exist
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+
+  // Serial number mismatches
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=dummy_serial",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + serialno,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+}
+
+TEST(InstallTest, CheckPackageMetadata_multiple_serial_number) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string serialno = android::base::GetProperty("ro.serialno", "");
+  ASSERT_NE("", serialno);
+
+  std::vector<std::string> serial_numbers;
+  // Creates a dummy serial number string.
+  for (char c = 'a'; c <= 'z'; c++) {
+    serial_numbers.emplace_back(serialno.size(), c);
+  }
+
+  // No matched serialno found.
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + android::base::Join(serial_numbers, '|'),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR);
+
+  serial_numbers.emplace_back(serialno);
+  std::shuffle(serial_numbers.begin(), serial_numbers.end(), std::default_random_engine());
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=BRICK",
+          "pre-device=" + device,
+          "serialno=" + android::base::Join(serial_numbers, '|'),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::BRICK, 0);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_build_version) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string build_version = android::base::GetProperty("ro.build.version.incremental", "");
+  ASSERT_NE("", build_version);
+
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build-incremental=" + build_version,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build-incremental=dummy_build",
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_fingerprint) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  std::string finger_print = android::base::GetProperty("ro.build.fingerprint", "");
+  ASSERT_NE("", finger_print);
+
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build=" + finger_print,
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "pre-build=dummy_build_fingerprint",
+          "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()),
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+}
+
+TEST(InstallTest, CheckPackageMetadata_ab_post_timestamp) {
+  std::string device = android::base::GetProperty("ro.product.device", "");
+  ASSERT_NE("", device);
+
+  // post timestamp is required for upgrade.
+  std::string metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // post timestamp should be larger than the timestamp on device.
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  // fingerprint is required for downgrade
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+          "ota-downgrade=yes",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR);
+
+  std::string finger_print = android::base::GetProperty("ro.build.fingerprint", "");
+  ASSERT_NE("", finger_print);
+
+  metadata = android::base::Join(
+      std::vector<std::string>{
+          "ota-type=AB",
+          "pre-device=" + device,
+          "post-timestamp=0",
+          "pre-build=" + finger_print,
+          "ota-downgrade=yes",
+      },
+      "\n");
+  test_check_package_metadata(metadata, OtaType::AB, 0);
+}
diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp
new file mode 100644
index 0000000..d7fdb8f
--- /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_GE(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/sideload_test.cpp b/tests/component/sideload_test.cpp
index b7109fc..d5e074c 100644
--- a/tests/component/sideload_test.cpp
+++ b/tests/component/sideload_test.cpp
@@ -21,7 +21,6 @@
 
 #include <android-base/file.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 
 #include "fuse_sideload.h"
diff --git a/tests/component/uncrypt_test.cpp b/tests/component/uncrypt_test.cpp
index 55baca2..e97d589 100644
--- a/tests/component/uncrypt_test.cpp
+++ b/tests/component/uncrypt_test.cpp
@@ -26,7 +26,6 @@
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/properties.h>
-#include <android-base/test_utils.h>
 #include <android-base/unique_fd.h>
 #include <bootloader_message/bootloader_message.h>
 #include <gtest/gtest.h>
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index 1544bb2..0a59403 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -14,29 +14,88 @@
  * 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/test_utils.h>
+#include <android-base/properties.h>
+#include <android-base/strings.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 +105,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 +136,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 +148,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..a0a7b66 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -23,38 +23,47 @@
 #include <algorithm>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <unordered_map>
 #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>
-#include <android-base/test_utils.h>
 #include <bootloader_message/bootloader_message.h>
 #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 std::string get_sha1(const std::string& content) {
+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 GetSha1(std::string_view content) {
   uint8_t digest[SHA_DIGEST_LENGTH];
-  SHA1(reinterpret_cast<const uint8_t*>(content.c_str()), content.size(), digest);
+  SHA1(reinterpret_cast<const uint8_t*>(content.data()), 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 += "/" + GetSha1(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 = GetSha1(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 = GetSha1(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,207 +601,198 @@
   ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
 }
 
-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');
+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);
+}
+
+// Generates the bsdiff of the given source and target images, and writes the result entries.
+// target_blocks specifies the block count to be written into the `bsdiff` command, which may be
+// different from the given target size in order to trigger overrun / underrun paths.
+static void GetEntriesForBsdiff(std::string_view source, std::string_view target,
+                                size_t target_blocks, PackageEntries* entries) {
   // 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*>(source.data()), source.size(),
+                              reinterpret_cast<const uint8_t*>(target.data()), target.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::string src_hash = GetSha1(source);
+  std::string tgt_hash = GetSha1(target);
+  size_t source_blocks = source.size() / 4096;
+  std::vector<std::string> transfer_list{
+    // clang-format off
+    "4",
+    std::to_string(target_blocks),
+    "0",
+    "0",
+    // bsdiff patch_offset patch_length source_hash target_hash target_range source_block_count
+    // source_range
+    android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,%zu %zu 2,0,%zu", patch_content.size(),
+                                src_hash.c_str(), tgt_hash.c_str(), target_blocks, source_blocks,
+                                source_blocks),
+    // clang-format on
+  };
+
+  *entries = {
+    { "new_data", "" },
+    { "patch_data", patch_content },
+    { "transfer_list", android::base::Join(transfer_list, '\n') },
+  };
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_data) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 2, &entries);
+  RunBlockImageUpdate(false, entries, image_file_, "t");
+
+  // The update_file should be patched correctly.
+  std::string updated;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
+  ASSERT_EQ(target, updated);
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_overrun) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  // Provide one less block to trigger the overrun path.
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 1, &entries);
+
+  // The update should fail due to overrun.
+  RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure);
+}
+
+TEST_F(UpdaterTest, block_image_update_patch_underrun) {
+  // Both source and target images have 10 blocks.
+  std::string source =
+      std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0');
+  std::string target =
+      std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0');
+  ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_));
+
+  // Provide one more block to trigger the overrun path.
+  PackageEntries entries;
+  GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2),
+                      std::string_view(target).substr(0, 4096 * 2), 3, &entries);
+
+  // The update should fail due to underrun.
+  RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure);
+}
+
+TEST_F(UpdaterTest, block_image_update_fail) {
+  std::string src_content(4096 * 2, 'e');
+  std::string src_hash = GetSha1(src_content);
+  // Stash and free some blocks, then fail the update intentionally.
+  std::vector<std::string> transfer_list{
+    // clang-format off
     "4",
     "2",
     "0",
     "2",
     "stash " + src_hash + " 2,0,2",
-    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,
-  };
-
-  std::unordered_map<std::string, std::string> 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());
-
-  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;
-
-  // 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);
-}
-
-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",
+    "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 = GetSha1(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 +825,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_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
-  std::string block3_hash = get_sha1(block3);
+  std::string block1(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(4096, '3');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+  std::string block3_hash = GetSha1(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 +856,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 +871,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_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
+  std::string block1(4096, '1');
+  std::string block2(4096, '2');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(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(4096, '1');
+  std::string block2(4096, '2');
+  std::string block3(4096, '3');
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+  std::string block3_hash = GetSha1(block3);
 
-  std::string block1 = std::string(4096, '1');
-  std::string block2 = std::string(4096, '2');
-  std::string block3 = std::string(4096, '3');
-  std::string block1_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
-  std::string block3_hash = get_sha1(block3);
-
-  std::vector<std::string> transfer_list_verify = {
+  std::vector<std::string> transfer_list_verify{
+    // clang-format off
     "4",
     "2",
     "0",
@@ -879,55 +961,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 += "/" + GetSha1(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 = GetSha1(a);
+  std::string b_hash = GetSha1(b);
+  std::string c_hash = GetSha1(c);
+  std::string e_hash = GetSha1(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 = GetSha1(bdc);
+  std::string g_hash = GetSha1(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 = GetSha1(abcd);
+  std::string dcb = d + c + b;
+  std::string dcb_hash = GetSha1(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..9fcaa0b 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>
@@ -27,14 +26,207 @@
 
 #include <android-base/file.h>
 #include <android-base/stringprintf.h>
-#include <android-base/test_utils.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <gtest/gtest.h>
+#include <openssl/bn.h>
+#include <openssl/ec.h>
+#include <openssl/nid.h>
+#include <ziparchive/zip_writer.h>
 
 #include "common/test_constants.h"
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
 #include "verifier.h"
 
 using namespace std::string_literals;
 
+static void LoadKeyFromFile(const std::string& file_name, Certificate* cert) {
+  std::string testkey_string;
+  ASSERT_TRUE(android::base::ReadFileToString(file_name, &testkey_string));
+  ASSERT_TRUE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), cert));
+}
+
+static void VerifyPackageWithCertificates(const std::string& name,
+                                          const std::vector<Certificate>& certs) {
+  std::string package = from_testdata_base(name);
+  MemMapping memmap;
+  if (!memmap.MapFile(package)) {
+    FAIL() << "Failed to mmap " << package << ": " << strerror(errno) << "\n";
+  }
+
+  ASSERT_EQ(VERIFY_SUCCESS, verify_file(memmap.addr, memmap.length, certs));
+}
+
+static void VerifyPackageWithSingleCertificate(const std::string& name, Certificate&& cert) {
+  std::vector<Certificate> certs;
+  certs.emplace_back(std::move(cert));
+  VerifyPackageWithCertificates(name, certs);
+}
+
+static void BuildCertificateArchive(const std::vector<std::string>& file_names, int fd) {
+  FILE* zip_file_ptr = fdopen(fd, "wb");
+  ZipWriter zip_writer(zip_file_ptr);
+
+  for (const auto& name : file_names) {
+    std::string content;
+    ASSERT_TRUE(android::base::ReadFileToString(name, &content));
+
+    // Makes sure the zip entry name has the correct suffix.
+    std::string entry_name = name;
+    if (!android::base::EndsWith(entry_name, "x509.pem")) {
+      entry_name += "x509.pem";
+    }
+    ASSERT_EQ(0, zip_writer.StartEntry(entry_name.c_str(), ZipWriter::kCompress));
+    ASSERT_EQ(0, zip_writer.WriteBytes(content.data(), content.size()));
+    ASSERT_EQ(0, zip_writer.FinishEntry());
+  }
+
+  ASSERT_EQ(0, zip_writer.Finish());
+  ASSERT_EQ(0, fclose(zip_file_ptr));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_failure) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  std::string testkey_string;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(from_testdata_base("testkey_v1.txt"), &testkey_string));
+  ASSERT_FALSE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), &cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v1.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v1.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v2.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v2.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v3.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v4.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithSingleCertificate("otasigned_v4.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_ec256bits) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v5.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_EC, cert.key_type);
+  ASSERT_EQ(nullptr, cert.rsa);
+
+  VerifyPackageWithSingleCertificate("otasigned_v5.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_check_rsa_keys) {
+  std::unique_ptr<RSA, RSADeleter> rsa(RSA_new());
+  std::unique_ptr<BIGNUM, decltype(&BN_free)> exponent(BN_new(), BN_free);
+  BN_set_word(exponent.get(), 3);
+  RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr);
+  ASSERT_TRUE(CheckRSAKey(rsa));
+
+  // Exponent is expected to be 3 or 65537
+  BN_set_word(exponent.get(), 17);
+  RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr);
+  ASSERT_FALSE(CheckRSAKey(rsa));
+
+  // Modulus is expected to be 2048.
+  BN_set_word(exponent.get(), 3);
+  RSA_generate_key_ex(rsa.get(), 1024, exponent.get(), nullptr);
+  ASSERT_FALSE(CheckRSAKey(rsa));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_check_ec_keys) {
+  std::unique_ptr<EC_KEY, ECKEYDeleter> ec(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1));
+  ASSERT_EQ(1, EC_KEY_generate_key(ec.get()));
+  ASSERT_TRUE(CheckECKey(ec));
+
+  // Expects 256-bit EC key with curve NIST P-256
+  ec.reset(EC_KEY_new_by_curve_name(NID_secp224r1));
+  ASSERT_EQ(1, EC_KEY_generate_key(ec.get()));
+  ASSERT_FALSE(CheckECKey(ec));
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_empty_archive) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive({}, otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_TRUE(certs.empty());
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_single_key) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive({ from_testdata_base("testkey_v1.x509.pem") }, otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(1, certs.size());
+
+  VerifyPackageWithCertificates("otasigned_v1.zip", certs);
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_corrupted_key) {
+  TemporaryFile corrupted_key;
+  std::string content;
+  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v1.x509.pem"), &content));
+  content = "random-contents" + content;
+  ASSERT_TRUE(android::base::WriteStringToFd(content, corrupted_key.release()));
+
+  TemporaryFile otacerts;
+  BuildCertificateArchive({ from_testdata_base("testkey_v2.x509.pem"), corrupted_key.path },
+                          otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(0, certs.size());
+}
+
+TEST(VerifierTest, LoadKeysFromZipfile_multiple_key) {
+  TemporaryFile otacerts;
+  BuildCertificateArchive(
+      {
+          from_testdata_base("testkey_v3.x509.pem"),
+          from_testdata_base("testkey_v4.x509.pem"),
+          from_testdata_base("testkey_v5.x509.pem"),
+
+      },
+      otacerts.release());
+  std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path);
+  ASSERT_EQ(3, certs.size());
+
+  VerifyPackageWithCertificates("otasigned_v3.zip", certs);
+  VerifyPackageWithCertificates("otasigned_v4.zip", certs);
+  VerifyPackageWithCertificates("otasigned_v5.zip", certs);
+}
+
 class VerifierTest : public testing::TestWithParam<std::vector<std::string>> {
  protected:
   void SetUp() override {
@@ -45,8 +237,9 @@
     }
 
     for (auto it = ++args.cbegin(); it != args.cend(); ++it) {
-      std::string public_key_file = from_testdata_base("testkey_" + *it + ".txt");
-      ASSERT_TRUE(load_keys(public_key_file.c_str(), certs));
+      std::string public_key_file = from_testdata_base("testkey_" + *it + ".x509.pem");
+      certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+      LoadKeyFromFile(public_key_file, &certs.back());
     }
   }
 
@@ -60,70 +253,10 @@
 class VerifierFailureTest : public VerifierTest {
 };
 
-TEST(VerifierTest, load_keys_multiple_keys) {
-  std::string testkey_v4;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v4.txt"), &testkey_v4));
-
-  std::string testkey_v3;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3));
-
-  std::string keys = testkey_v4 + "," + testkey_v3 + "," + testkey_v4;
-  TemporaryFile key_file1;
-  ASSERT_TRUE(android::base::WriteStringToFile(keys, key_file1.path));
-  std::vector<Certificate> certs;
-  ASSERT_TRUE(load_keys(key_file1.path, certs));
-  ASSERT_EQ(3U, certs.size());
-}
-
-TEST(VerifierTest, load_keys_invalid_keys) {
-  std::vector<Certificate> certs;
-  ASSERT_FALSE(load_keys("/doesntexist", certs));
-
-  // Empty file.
-  TemporaryFile key_file1;
-  ASSERT_FALSE(load_keys(key_file1.path, certs));
-
-  // Invalid contents.
-  ASSERT_TRUE(android::base::WriteStringToFile("invalid", key_file1.path));
-  ASSERT_FALSE(load_keys(key_file1.path, certs));
-
-  std::string testkey_v4;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v4.txt"), &testkey_v4));
-
-  // Invalid key version: "v4 ..." => "v6 ...".
-  std::string invalid_key2(testkey_v4);
-  invalid_key2[1] = '6';
-  TemporaryFile key_file2;
-  ASSERT_TRUE(android::base::WriteStringToFile(invalid_key2, key_file2.path));
-  ASSERT_FALSE(load_keys(key_file2.path, certs));
-
-  // Invalid key content: inserted extra bytes ",2209831334".
-  std::string invalid_key3(testkey_v4);
-  invalid_key3.insert(invalid_key2.size() - 2, ",2209831334");
-  TemporaryFile key_file3;
-  ASSERT_TRUE(android::base::WriteStringToFile(invalid_key3, key_file3.path));
-  ASSERT_FALSE(load_keys(key_file3.path, certs));
-
-  // Invalid key: the last key must not end with an extra ','.
-  std::string invalid_key4 = testkey_v4 + ",";
-  TemporaryFile key_file4;
-  ASSERT_TRUE(android::base::WriteStringToFile(invalid_key4, key_file4.path));
-  ASSERT_FALSE(load_keys(key_file4.path, certs));
-
-  // Invalid key separator.
-  std::string invalid_key5 = testkey_v4 + ";" + testkey_v4;
-  TemporaryFile key_file5;
-  ASSERT_TRUE(android::base::WriteStringToFile(invalid_key5, key_file5.path));
-  ASSERT_FALSE(load_keys(key_file5.path, certs));
-}
-
 TEST(VerifierTest, BadPackage_AlteredFooter) {
-  std::string testkey_v3;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3));
-  TemporaryFile key_file1;
-  ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file1.path));
   std::vector<Certificate> certs;
-  ASSERT_TRUE(load_keys(key_file1.path, certs));
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
 
   std::string package;
   ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package));
@@ -137,12 +270,9 @@
 }
 
 TEST(VerifierTest, BadPackage_AlteredContent) {
-  std::string testkey_v3;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3));
-  TemporaryFile key_file1;
-  ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file1.path));
   std::vector<Certificate> certs;
-  ASSERT_TRUE(load_keys(key_file1.path, certs));
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
 
   std::string package;
   ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package));
@@ -163,13 +293,9 @@
 }
 
 TEST(VerifierTest, BadPackage_SignatureStartOutOfBounds) {
-  std::string testkey_v3;
-  ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3));
-
-  TemporaryFile key_file;
-  ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file.path));
   std::vector<Certificate> certs;
-  ASSERT_TRUE(load_keys(key_file.path, certs));
+  certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back());
 
   // Signature start is 65535 (0xffff) while comment size is 0 (Bug: 31914369).
   std::string package = "\x50\x4b\x05\x06"s + std::string(12, '\0') + "\xff\xff\xff\xff\x00\x00"s;
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/battery_scale.png b/tests/testdata/battery_scale.png
new file mode 100644
index 0000000..2ae8f0f
--- /dev/null
+++ b/tests/testdata/battery_scale.png
Binary files differ
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/jarsigned.zip b/tests/testdata/jarsigned.zip
deleted file mode 100644
index 8b1ef8b..0000000
--- a/tests/testdata/jarsigned.zip
+++ /dev/null
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/testdata/patch.bsdiff b/tests/testdata/patch.bsdiff
deleted file mode 100644
index b78d385..0000000
--- a/tests/testdata/patch.bsdiff
+++ /dev/null
Binary files differ
diff --git a/tests/testdata/unsigned.zip b/tests/testdata/unsigned.zip
deleted file mode 100644
index 24e3ead..0000000
--- a/tests/testdata/unsigned.zip
+++ /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..794f2c1
--- /dev/null
+++ b/tests/unit/applypatch_test.cpp
@@ -0,0 +1,290 @@
+/*
+ * 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/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..4dd111a 100644
--- a/tests/unit/dirutil_test.cpp
+++ b/tests/unit/dirutil_test.cpp
@@ -20,9 +20,10 @@
 
 #include <string>
 
-#include <android-base/test_utils.h>
+#include <android-base/file.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/minui_test.cpp b/tests/unit/minui_test.cpp
new file mode 100644
index 0000000..c7d7f7e
--- /dev/null
+++ b/tests/unit/minui_test.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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 <stdint.h>
+#include <stdlib.h>
+
+#include <limits>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+#include "minui/minui.h"
+
+TEST(GRSurfaceTest, Create_aligned) {
+  auto surface = GRSurface::Create(9, 11, 9, 1);
+  ASSERT_TRUE(surface);
+  ASSERT_EQ(0, reinterpret_cast<uintptr_t>(surface->data()) % GRSurface::kSurfaceDataAlignment);
+  // data_size will be rounded up to the next multiple of GRSurface::kSurfaceDataAlignment.
+  ASSERT_EQ(0, surface->data_size() % GRSurface::kSurfaceDataAlignment);
+  ASSERT_GE(surface->data_size(), 11 * 9);
+}
+
+TEST(GRSurfaceTest, Create_invalid_inputs) {
+  ASSERT_FALSE(GRSurface::Create(9, 11, 0, 1));
+  ASSERT_FALSE(GRSurface::Create(9, 0, 9, 1));
+  ASSERT_FALSE(GRSurface::Create(0, 11, 9, 1));
+  ASSERT_FALSE(GRSurface::Create(9, 11, 9, 0));
+  ASSERT_FALSE(GRSurface::Create(9, 101, std::numeric_limits<size_t>::max() / 100, 1));
+}
+
+TEST(GRSurfaceTest, Clone) {
+  auto image = GRSurface::Create(50, 10, 50, 1);
+  ASSERT_GE(image->data_size(), 10 * 50);
+  for (auto i = 0; i < image->data_size(); i++) {
+    image->data()[i] = rand() % 128;
+  }
+  auto image_copy = image->Clone();
+  ASSERT_EQ(image->data_size(), image_copy->data_size());
+  ASSERT_EQ(std::vector(image->data(), image->data() + image->data_size()),
+            std::vector(image_copy->data(), image_copy->data() + image->data_size()));
+}
diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp
new file mode 100644
index 0000000..72169a0
--- /dev/null
+++ b/tests/unit/parse_install_logs_test.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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 <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/resources_test.cpp b/tests/unit/resources_test.cpp
new file mode 100644
index 0000000..c3f7271
--- /dev/null
+++ b/tests/unit/resources_test.cpp
@@ -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.
+ */
+
+#include <string>
+
+#include <gtest/gtest.h>
+
+#include "common/test_constants.h"
+#include "minui/minui.h"
+
+TEST(ResourcesTest, res_create_multi_display_surface) {
+  GRSurface** frames;
+  int frame_count;
+  int fps;
+  ASSERT_EQ(0, res_create_multi_display_surface(from_testdata_base("battery_scale.png").c_str(),
+                                                &frame_count, &fps, &frames));
+  ASSERT_EQ(6, frame_count);
+  ASSERT_EQ(20, fps);
+
+  for (auto i = 0; i < frame_count; i++) {
+    free(frames[i]);
+  }
+  free(frames);
+}
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
new file mode 100644
index 0000000..647c7b2
--- /dev/null
+++ b/tests/unit/screen_ui_test.cpp
@@ -0,0 +1,562 @@
+/*
+ * 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/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <gtest/gtest.h>
+#include <gtest/gtest_prod.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" };
+
+// TODO(xunchang) check if some draw functions are called when drawing menus.
+class MockDrawFunctions : public DrawInterface {
+  void SetColor(UIElement /* element */) const override {}
+  void DrawHighlightBar(int /* x */, int /* y */, int /* width */,
+                        int /* height */) const override {}
+  int DrawHorizontalRule(int /* y */) const override {
+    return 0;
+  }
+  int DrawTextLine(int /* x */, int /* y */, const std::string& /* line */,
+                   bool /* bold */) const override {
+    return 0;
+  }
+  void DrawSurface(const GRSurface* /* surface */, int /* sx */, int /* sy */, int /* w */,
+                   int /* h */, int /* dx */, int /* dy */) const override {}
+  void DrawFill(int /* x */, int /* y */, int /* w */, int /* h */) const override {}
+  void DrawTextIcon(int /* x */, int /* y */, const GRSurface* /* surface */) const override {}
+  int DrawTextLines(int /* x */, int /* y */,
+                    const std::vector<std::string>& /* lines */) const override {
+    return 0;
+  }
+  int DrawWrappedTextLines(int /* x */, int /* y */,
+                           const std::vector<std::string>& /* lines */) const override {
+    return 0;
+  }
+};
+
+class ScreenUITest : public testing::Test {
+ protected:
+  MockDrawFunctions draw_funcs_;
+};
+
+TEST_F(ScreenUITest, StartPhoneMenuSmoke) {
+  TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
+  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_F(ScreenUITest, StartWearMenuSmoke) {
+  TextMenu menu(true, 10, 8, HEADERS, ITEMS, 1, 20, draw_funcs_);
+  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_F(ScreenUITest, StartPhoneMenuItemsOverflow) {
+  TextMenu menu(false, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
+  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_F(ScreenUITest, StartWearMenuItemsOverflow) {
+  TextMenu menu(true, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
+  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_F(ScreenUITest, PhoneMenuSelectSmoke) {
+  int sel = 0;
+  TextMenu menu(false, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
+  // 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_F(ScreenUITest, WearMenuSelectSmoke) {
+  int sel = 0;
+  TextMenu menu(true, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
+  // 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_F(ScreenUITest, WearMenuSelectItemsOverflow) {
+  int sel = 1;
+  TextMenu menu(true, 3, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
+  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());
+}
+
+TEST_F(ScreenUITest, GraphicMenuSelection) {
+  auto image = GRSurface::Create(50, 50, 50, 1);
+  auto header = image->Clone();
+  std::vector<const GRSurface*> items = {
+    image.get(),
+    image.get(),
+    image.get(),
+  };
+  GraphicMenu menu(header.get(), items, 0, draw_funcs_);
+
+  ASSERT_EQ(0, menu.selection());
+
+  int sel = 0;
+  for (int i = 0; i < 3; i++) {
+    sel = menu.Select(++sel);
+    ASSERT_EQ((i + 1) % 3, sel);
+    ASSERT_EQ(sel, menu.selection());
+  }
+
+  sel = 0;
+  for (int i = 0; i < 3; i++) {
+    sel = menu.Select(--sel);
+    ASSERT_EQ(2 - i, sel);
+    ASSERT_EQ(sel, menu.selection());
+  }
+}
+
+TEST_F(ScreenUITest, GraphicMenuValidate) {
+  auto image = GRSurface::Create(50, 50, 50, 1);
+  auto header = image->Clone();
+  std::vector<const GRSurface*> items = {
+    image.get(),
+    image.get(),
+    image.get(),
+  };
+
+  ASSERT_TRUE(GraphicMenu::Validate(200, 200, header.get(), items));
+
+  // Menu exceeds the horizontal boundary.
+  auto wide_surface = GRSurface::Create(300, 50, 300, 1);
+  ASSERT_FALSE(GraphicMenu::Validate(299, 200, wide_surface.get(), items));
+
+  // Menu exceeds the vertical boundary.
+  items.emplace_back(image.get());
+  ASSERT_FALSE(GraphicMenu::Validate(200, 249, header.get(), items));
+}
+
+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;
+
+ private:
+  FRIEND_TEST(DISABLED_ScreenRecoveryUITest, Init);
+  FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocale);
+  FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix);
+  FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation);
+  FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation);
+
+  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 DISABLED_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(DISABLED_ScreenRecoveryUITest, Init) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestLocale));
+  ASSERT_EQ(kTestLocale, ui_->GetLocale());
+  ASSERT_FALSE(ui_->rtl_locale_);
+  ASSERT_FALSE(ui_->IsTextVisible());
+  ASSERT_FALSE(ui_->WasTextEverVisible());
+}
+
+TEST_F(DISABLED_ScreenRecoveryUITest, dtor_NotCallingInit) {
+  ui_.reset();
+  ASSERT_FALSE(ui_);
+}
+
+TEST_F(DISABLED_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(DISABLED_ScreenRecoveryUITest, RtlLocale) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestRtlLocale));
+  ASSERT_TRUE(ui_->rtl_locale_);
+}
+
+TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix) {
+  RETURN_IF_NO_GRAPHICS;
+
+  ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix));
+  ASSERT_TRUE(ui_->rtl_locale_);
+}
+
+TEST_F(DISABLED_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(DISABLED_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(DISABLED_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(DISABLED_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(DISABLED_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(DISABLED_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_->LoadAnimation();
+
+  ASSERT_EQ(2u, ui_->intro_frames_.size());
+  ASSERT_EQ(3u, ui_->loop_frames_.size());
+
+  for (const auto& name : tempfiles) {
+    ASSERT_EQ(0, unlink(name.c_str()));
+  }
+}
+
+TEST_F(DISABLED_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_->LoadAnimation(), ::testing::KilledBySignal(SIGABRT), "");
+}
+
+#undef RETURN_IF_NO_GRAPHICS
diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp
index 434ee25..77625db 100644
--- a/tests/unit/sysutil_test.cpp
+++ b/tests/unit/sysutil_test.cpp
@@ -14,14 +14,12 @@
  * 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 +126,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..dfe617e 100644
--- a/tests/unit/zip_test.cpp
+++ b/tests/unit/zip_test.cpp
@@ -21,12 +21,11 @@
 #include <vector>
 
 #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/DumpPublicKey.java b/tools/dumpkey/DumpPublicKey.java
deleted file mode 100644
index 3eb1398..0000000
--- a/tools/dumpkey/DumpPublicKey.java
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.dumpkey;
-
-import org.bouncycastle.jce.provider.BouncyCastleProvider;
-
-import java.io.FileInputStream;
-import java.math.BigInteger;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.KeyStore;
-import java.security.Key;
-import java.security.PublicKey;
-import java.security.Security;
-import java.security.interfaces.ECPublicKey;
-import java.security.interfaces.RSAPublicKey;
-import java.security.spec.ECPoint;
-
-/**
- * Command line tool to extract RSA public keys from X.509 certificates
- * and output source code with data initializers for the keys.
- * @hide
- */
-class DumpPublicKey {
-    /**
-     * @param key to perform sanity checks on
-     * @return version number of key.  Supported versions are:
-     *     1: 2048-bit RSA key with e=3 and SHA-1 hash
-     *     2: 2048-bit RSA key with e=65537 and SHA-1 hash
-     *     3: 2048-bit RSA key with e=3 and SHA-256 hash
-     *     4: 2048-bit RSA key with e=65537 and SHA-256 hash
-     * @throws Exception if the key has the wrong size or public exponent
-     */
-    static int checkRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
-        BigInteger pubexp = key.getPublicExponent();
-        BigInteger modulus = key.getModulus();
-        int version;
-
-        if (pubexp.equals(BigInteger.valueOf(3))) {
-            version = useSHA256 ? 3 : 1;
-        } else if (pubexp.equals(BigInteger.valueOf(65537))) {
-            version = useSHA256 ? 4 : 2;
-        } else {
-            throw new Exception("Public exponent should be 3 or 65537 but is " +
-                                pubexp.toString(10) + ".");
-        }
-
-        if (modulus.bitLength() != 2048) {
-             throw new Exception("Modulus should be 2048 bits long but is " +
-                        modulus.bitLength() + " bits.");
-        }
-
-        return version;
-    }
-
-    /**
-     * @param key to perform sanity checks on
-     * @return version number of key.  Supported versions are:
-     *     5: 256-bit EC key with curve NIST P-256
-     * @throws Exception if the key has the wrong size or public exponent
-     */
-    static int checkEC(ECPublicKey key) throws Exception {
-        if (key.getParams().getCurve().getField().getFieldSize() != 256) {
-            throw new Exception("Curve must be NIST P-256");
-        }
-
-        return 5;
-    }
-
-    /**
-     * Perform sanity check on public key.
-     */
-    static int check(PublicKey key, boolean useSHA256) throws Exception {
-        if (key instanceof RSAPublicKey) {
-            return checkRSA((RSAPublicKey) key, useSHA256);
-        } else if (key instanceof ECPublicKey) {
-            if (!useSHA256) {
-                throw new Exception("Must use SHA-256 with EC keys!");
-            }
-            return checkEC((ECPublicKey) key);
-        } else {
-            throw new Exception("Unsupported key class: " + key.getClass().getName());
-        }
-    }
-
-    /**
-     * @param key to output
-     * @return a String representing this public key.  If the key is a
-     *    version 1 key, the string will be a C initializer; this is
-     *    not true for newer key versions.
-     */
-    static String printRSA(RSAPublicKey key, boolean useSHA256) throws Exception {
-        int version = check(key, useSHA256);
-
-        BigInteger N = key.getModulus();
-
-        StringBuilder result = new StringBuilder();
-
-        int nwords = N.bitLength() / 32;    // # of 32 bit integers in modulus
-
-        if (version > 1) {
-            result.append("v");
-            result.append(Integer.toString(version));
-            result.append(" ");
-        }
-
-        result.append("{");
-        result.append(nwords);
-
-        BigInteger B = BigInteger.valueOf(0x100000000L);  // 2^32
-        BigInteger N0inv = B.subtract(N.modInverse(B));   // -1 / N[0] mod 2^32
-
-        result.append(",0x");
-        result.append(N0inv.toString(16));
-
-        BigInteger R = BigInteger.valueOf(2).pow(N.bitLength());
-        BigInteger RR = R.multiply(R).mod(N);    // 2^4096 mod N
-
-        // Write out modulus as little endian array of integers.
-        result.append(",{");
-        for (int i = 0; i < nwords; ++i) {
-            long n = N.mod(B).longValue();
-            result.append(n);
-
-            if (i != nwords - 1) {
-                result.append(",");
-            }
-
-            N = N.divide(B);
-        }
-        result.append("}");
-
-        // Write R^2 as little endian array of integers.
-        result.append(",{");
-        for (int i = 0; i < nwords; ++i) {
-            long rr = RR.mod(B).longValue();
-            result.append(rr);
-
-            if (i != nwords - 1) {
-                result.append(",");
-            }
-
-            RR = RR.divide(B);
-        }
-        result.append("}");
-
-        result.append("}");
-        return result.toString();
-    }
-
-    /**
-     * @param key to output
-     * @return a String representing this public key.  If the key is a
-     *    version 1 key, the string will be a C initializer; this is
-     *    not true for newer key versions.
-     */
-    static String printEC(ECPublicKey key) throws Exception {
-        int version = checkEC(key);
-
-        StringBuilder result = new StringBuilder();
-
-        result.append("v");
-        result.append(Integer.toString(version));
-        result.append(" ");
-
-        BigInteger X = key.getW().getAffineX();
-        BigInteger Y = key.getW().getAffineY();
-        int nbytes = key.getParams().getCurve().getField().getFieldSize() / 8;    // # of 32 bit integers in X coordinate
-
-        result.append("{");
-        result.append(nbytes);
-
-        BigInteger B = BigInteger.valueOf(0x100L);  // 2^8
-
-        // Write out Y coordinate as array of characters.
-        result.append(",{");
-        for (int i = 0; i < nbytes; ++i) {
-            long n = X.mod(B).longValue();
-            result.append(n);
-
-            if (i != nbytes - 1) {
-                result.append(",");
-            }
-
-            X = X.divide(B);
-        }
-        result.append("}");
-
-        // Write out Y coordinate as array of characters.
-        result.append(",{");
-        for (int i = 0; i < nbytes; ++i) {
-            long n = Y.mod(B).longValue();
-            result.append(n);
-
-            if (i != nbytes - 1) {
-                result.append(",");
-            }
-
-            Y = Y.divide(B);
-        }
-        result.append("}");
-
-        result.append("}");
-        return result.toString();
-    }
-
-    static String print(PublicKey key, boolean useSHA256) throws Exception {
-        if (key instanceof RSAPublicKey) {
-            return printRSA((RSAPublicKey) key, useSHA256);
-        } else if (key instanceof ECPublicKey) {
-            return printEC((ECPublicKey) key);
-        } else {
-            throw new Exception("Unsupported key class: " + key.getClass().getName());
-        }
-    }
-
-    public static void main(String[] args) {
-        if (args.length < 1) {
-            System.err.println("Usage: DumpPublicKey certfile ... > source.c");
-            System.exit(1);
-        }
-        Security.addProvider(new BouncyCastleProvider());
-        try {
-            for (int i = 0; i < args.length; i++) {
-                FileInputStream input = new FileInputStream(args[i]);
-                CertificateFactory cf = CertificateFactory.getInstance("X.509");
-                X509Certificate cert = (X509Certificate) cf.generateCertificate(input);
-
-                boolean useSHA256 = false;
-                String sigAlg = cert.getSigAlgName();
-                if ("SHA1withRSA".equals(sigAlg) || "MD5withRSA".equals(sigAlg)) {
-                    // SignApk has historically accepted "MD5withRSA"
-                    // certificates, but treated them as "SHA1withRSA"
-                    // anyway.  Continue to do so for backwards
-                    // compatibility.
-                  useSHA256 = false;
-                } else if ("SHA256withRSA".equals(sigAlg) || "SHA256withECDSA".equals(sigAlg)) {
-                  useSHA256 = true;
-                } else {
-                  System.err.println(args[i] + ": unsupported signature algorithm \"" +
-                                     sigAlg + "\"");
-                  System.exit(1);
-                }
-
-                PublicKey key = cert.getPublicKey();
-                check(key, useSHA256);
-                System.out.print(print(key, useSHA256));
-                System.out.println(i < args.length - 1 ? "," : "");
-            }
-        } catch (Exception e) {
-            e.printStackTrace();
-            System.exit(1);
-        }
-        System.exit(0);
-    }
-}
diff --git a/tools/dumpkey/DumpPublicKey.mf b/tools/dumpkey/DumpPublicKey.mf
deleted file mode 100644
index 7bb3bc8..0000000
--- a/tools/dumpkey/DumpPublicKey.mf
+++ /dev/null
@@ -1 +0,0 @@
-Main-Class: com.android.dumpkey.DumpPublicKey
diff --git a/tools/image_generator/Android.bp b/tools/image_generator/Android.bp
new file mode 100644
index 0000000..2afdd5a
--- /dev/null
+++ b/tools/image_generator/Android.bp
@@ -0,0 +1,28 @@
+// 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: "RecoveryImageGenerator",
+
+    manifest: "ImageGenerator.mf",
+
+    static_libs: [
+        "commons-cli-1.2",
+        "icu4j-host",
+    ],
+
+    srcs: [
+        "ImageGenerator.java",
+    ],
+}
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
new file mode 100644
index 0000000..fd8e542
--- /dev/null
+++ b/tools/image_generator/ImageGenerator.java
@@ -0,0 +1,757 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.recovery.tools;
+
+import com.ibm.icu.text.BreakIterator;
+
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.GnuParser;
+import org.apache.commons.cli.HelpFormatter;
+import org.apache.commons.cli.OptionBuilder;
+import org.apache.commons.cli.Options;
+import org.apache.commons.cli.ParseException;
+import org.w3c.dom.Document;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.font.TextAttribute;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.text.AttributedString;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/** Command line tool to generate the localized image for recovery mode. */
+public class ImageGenerator {
+    // Initial height of the image to draw.
+    private static final int INITIAL_HEIGHT = 20000;
+
+    private static final float DEFAULT_FONT_SIZE = 40;
+
+    private static final Logger LOGGER = Logger.getLogger(ImageGenerator.class.getName());
+
+    // This is the canvas we used to draw texts.
+    private BufferedImage mBufferedImage;
+
+    // The width in pixels of our image. The value will be adjusted once when we calculate the
+    // maximum width to fit the wrapped text strings.
+    private int mImageWidth;
+
+    // The current height in pixels of our image. We will adjust the value when drawing more texts.
+    private int mImageHeight;
+
+    // The current vertical offset in pixels to draw the top edge of new text strings.
+    private int mVerticalOffset;
+
+    // The font size to draw the texts.
+    private final float mFontSize;
+
+    // The name description of the text to localize. It's used to find the translated strings in the
+    // resource file.
+    private final String mTextName;
+
+    // The directory that contains all the needed font files (e.g. ttf, otf, ttc files).
+    private final String mFontDirPath;
+
+    // Align the text in the center of the image.
+    private final boolean mCenterAlignment;
+
+    // Some localized font cannot draw the word "Android" and some PUNCTUATIONS; we need to fall
+    // back to use our default latin font instead.
+    private static final char[] PUNCTUATIONS = {',', ';', '.', '!', '?'};
+
+    private static final String ANDROID_STRING = "Android";
+
+    // The width of the word "Android" when drawing with the default font.
+    private int mAndroidStringWidth;
+
+    // The default Font to draw latin characters. It's loaded from DEFAULT_FONT_NAME.
+    private Font mDefaultFont;
+    // Cache of the loaded fonts for all languages.
+    private Map<String, Font> mLoadedFontMap;
+
+    // An explicit map from language to the font name to use.
+    // The map is extracted from frameworks/base/data/fonts/fonts.xml.
+    // And the language-subtag-registry is found in:
+    // https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry
+    private static final String DEFAULT_FONT_NAME = "Roboto-Regular";
+    private static final Map<String, String> LANGUAGE_TO_FONT_MAP =
+            new TreeMap<String, String>() {
+                {
+                    put("am", "NotoSansEthiopic-Regular");
+                    put("ar", "NotoNaskhArabicUI-Regular");
+                    put("as", "NotoSansBengaliUI-Regular");
+                    put("bn", "NotoSansBengaliUI-Regular");
+                    put("fa", "NotoNaskhArabicUI-Regular");
+                    put("gu", "NotoSansGujaratiUI-Regular");
+                    put("hi", "NotoSansDevanagariUI-Regular");
+                    put("hy", "NotoSansArmenian-Regular");
+                    put("iw", "NotoSansHebrew-Regular");
+                    put("ja", "NotoSansCJK-Regular");
+                    put("ka", "NotoSansGeorgian-Regular");
+                    put("ko", "NotoSansCJK-Regular");
+                    put("km", "NotoSansKhmerUI-Regular");
+                    put("kn", "NotoSansKannadaUI-Regular");
+                    put("lo", "NotoSansLaoUI-Regular");
+                    put("ml", "NotoSansMalayalamUI-Regular");
+                    put("mr", "NotoSansDevanagariUI-Regular");
+                    put("my", "NotoSansMyanmarUI-Regular");
+                    put("ne", "NotoSansDevanagariUI-Regular");
+                    put("or", "NotoSansOriya-Regular");
+                    put("pa", "NotoSansGurmukhiUI-Regular");
+                    put("si", "NotoSansSinhala-Regular");
+                    put("ta", "NotoSansTamilUI-Regular");
+                    put("te", "NotoSansTeluguUI-Regular");
+                    put("th", "NotoSansThaiUI-Regular");
+                    put("ur", "NotoNaskhArabicUI-Regular");
+                    put("zh", "NotoSansCJK-Regular");
+                }
+            };
+
+    // Languages that write from right to left.
+    private static final Set<String> RTL_LANGUAGE =
+            new HashSet<String>() {
+                {
+                    add("ar"); // Arabic
+                    add("fa"); // Persian
+                    add("he"); // Hebrew
+                    add("iw"); // Hebrew
+                    add("ur"); // Urdu
+                }
+            };
+
+    /** Exception to indicate the failure to find the translated text strings. */
+    public static class LocalizedStringNotFoundException extends Exception {
+        public LocalizedStringNotFoundException(String message) {
+            super(message);
+        }
+
+        public LocalizedStringNotFoundException(String message, Throwable cause) {
+            super(message, cause);
+        }
+    }
+
+    /**
+     *  This class maintains the content of wrapped text, the attributes to draw these text, and
+     *  the width of each wrapped lines.
+     */
+    private class WrappedTextInfo {
+        /** LineInfo holds the AttributedString and width of each wrapped line. */
+        private class LineInfo {
+            public AttributedString mLineContent;
+            public int mLineWidth;
+
+            LineInfo(AttributedString text, int width) {
+                mLineContent = text;
+                mLineWidth = width;
+            }
+        }
+
+        // Maintains the content of each line, as well as the width needed to draw these lines for
+        // a given language.
+        public List<LineInfo> mWrappedLines;
+
+        WrappedTextInfo() {
+            mWrappedLines = new ArrayList<>();
+        }
+
+        /**
+         * Checks if the given text has words "Android" and some PUNCTUATIONS. If it does, and its
+         * associated textFont cannot display them correctly (e.g. for persian and hebrew); sets the
+         * attributes of these substrings to use our default font instead.
+         *
+         * @param text the input string to perform the check on
+         * @param width the pre-calculated width for the given text
+         * @param textFont the localized font to draw the input string
+         * @param fallbackFont our default font to draw latin characters
+         */
+        public void addLine(String text, int width, Font textFont, Font fallbackFont) {
+            AttributedString attributedText = new AttributedString(text);
+            attributedText.addAttribute(TextAttribute.FONT, textFont);
+            attributedText.addAttribute(TextAttribute.SIZE, mFontSize);
+
+            // Skips the check if we don't specify a fallbackFont.
+            if (fallbackFont != null) {
+                // Adds the attribute to use default font to draw the word "Android".
+                if (text.contains(ANDROID_STRING)
+                        && textFont.canDisplayUpTo(ANDROID_STRING) != -1) {
+                    int index = text.indexOf(ANDROID_STRING);
+                    attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index,
+                            index + ANDROID_STRING.length());
+                }
+
+                // Adds the attribute to use default font to draw the PUNCTUATIONS ", . ; ! ?"
+                for (char punctuation : PUNCTUATIONS) {
+                    // TODO (xunchang) handle the RTL language that has different directions for '?'
+                    if (text.indexOf(punctuation) != -1 && !textFont.canDisplay(punctuation)) {
+                        int index = 0;
+                        while ((index = text.indexOf(punctuation, index)) != -1) {
+                            attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index,
+                                    index + 1);
+                            index += 1;
+                        }
+                    }
+                }
+            }
+
+            mWrappedLines.add(new LineInfo(attributedText, width));
+        }
+
+        /** Merges two WrappedTextInfo. */
+        public void addLines(WrappedTextInfo other) {
+            mWrappedLines.addAll(other.mWrappedLines);
+        }
+    }
+
+    /** Initailizes the fields of the image image. */
+    public ImageGenerator(
+            int initialImageWidth,
+            String textName,
+            float fontSize,
+            String fontDirPath,
+            boolean centerAlignment) {
+        mImageWidth = initialImageWidth;
+        mImageHeight = INITIAL_HEIGHT;
+        mVerticalOffset = 0;
+
+        // Initialize the canvas with the default height.
+        mBufferedImage = new BufferedImage(mImageWidth, mImageHeight, BufferedImage.TYPE_BYTE_GRAY);
+
+        mTextName = textName;
+        mFontSize = fontSize;
+        mFontDirPath = fontDirPath;
+        mLoadedFontMap = new TreeMap<>();
+
+        mCenterAlignment = centerAlignment;
+    }
+
+    /**
+     * Finds the translated text string for the given textName by parsing the resourceFile. Example
+     * of the xml fields: <resources xmlns:android="http://schemas.android.com/apk/res/android">
+     * <string name="recovery_installing_security" msgid="9184031299717114342"> "Sicherheitsupdate
+     * wird installiert"</string> </resources>
+     *
+     * @param resourceFile the input resource file in xml format.
+     * @param textName the name description of the text.
+     * @return the string representation of the translated text.
+     */
+    private String getTextString(File resourceFile, String textName)
+            throws IOException, ParserConfigurationException, org.xml.sax.SAXException,
+                    LocalizedStringNotFoundException {
+        DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance();
+        DocumentBuilder db = builder.newDocumentBuilder();
+
+        Document doc = db.parse(resourceFile);
+        doc.getDocumentElement().normalize();
+
+        NodeList nodeList = doc.getElementsByTagName("string");
+        for (int i = 0; i < nodeList.getLength(); i++) {
+            Node node = nodeList.item(i);
+            String name = node.getAttributes().getNamedItem("name").getNodeValue();
+            if (name.equals(textName)) {
+                return node.getTextContent();
+            }
+        }
+
+        throw new LocalizedStringNotFoundException(
+                textName + " not found in " + resourceFile.getName());
+    }
+
+    /** Constructs the locale from the name of the resource file. */
+    private Locale getLocaleFromFilename(String filename) throws IOException {
+        // Gets the locale string by trimming the top "values-".
+        String localeString = filename.substring(7);
+        if (localeString.matches("[A-Za-z]+")) {
+            return Locale.forLanguageTag(localeString);
+        }
+        if (localeString.matches("[A-Za-z]+-r[A-Za-z]+")) {
+            // "${Language}-r${Region}". e.g. en-rGB
+            String[] tokens = localeString.split("-r");
+            return Locale.forLanguageTag(String.join("-", tokens));
+        }
+        if (localeString.startsWith("b+")) {
+            // The special case of b+sr+Latn, which has the form "b+${Language}+${ScriptName}"
+            String[] tokens = localeString.substring(2).split("\\+");
+            return Locale.forLanguageTag(String.join("-", tokens));
+        }
+
+        throw new IOException("Unrecognized locale string " + localeString);
+    }
+
+    /**
+     * Iterates over the xml files in the format of values-$LOCALE/strings.xml under the resource
+     * directory and collect the translated text.
+     *
+     * @param resourcePath the path to the resource directory
+     * @param localesSet a list of supported locales; resources of other locales will be omitted.
+     * @return a map with the locale as key, and translated text as value
+     * @throws LocalizedStringNotFoundException if we cannot find the translated text for the given
+     *     locale
+     */
+    public Map<Locale, String> readLocalizedStringFromXmls(String resourcePath,
+            Set<String> localesSet) throws IOException, LocalizedStringNotFoundException {
+        File resourceDir = new File(resourcePath);
+        if (!resourceDir.isDirectory()) {
+            throw new LocalizedStringNotFoundException(resourcePath + " is not a directory.");
+        }
+
+        Map<Locale, String> result =
+                // Overrides the string comparator so that sr is sorted behind sr-Latn. And thus
+                // recovery can find the most relevant locale when going down the list.
+                new TreeMap<>(
+                        (Locale l1, Locale l2) -> {
+                            if (l1.toLanguageTag().equals(l2.toLanguageTag())) {
+                                return 0;
+                            }
+                            if (l1.getLanguage().equals(l2.toLanguageTag())) {
+                                return -1;
+                            }
+                            if (l2.getLanguage().equals(l1.toLanguageTag())) {
+                                return 1;
+                            }
+                            return l1.toLanguageTag().compareTo(l2.toLanguageTag());
+                        });
+
+        // Find all the localized resource subdirectories in the format of values-$LOCALE
+        String[] nameList =
+                resourceDir.list((File file, String name) -> name.startsWith("values-"));
+        for (String name : nameList) {
+            String localeString = name.substring(7);
+            if (localesSet != null && !localesSet.contains(localeString)) {
+                LOGGER.info("Skip parsing text for locale " + localeString);
+                continue;
+            }
+
+            File textFile = new File(resourcePath, name + "/strings.xml");
+            String localizedText;
+            try {
+                localizedText = getTextString(textFile, mTextName);
+            } catch (IOException | ParserConfigurationException | org.xml.sax.SAXException e) {
+                throw new LocalizedStringNotFoundException(
+                        "Failed to read the translated text for locale " + name, e);
+            }
+
+            Locale locale = getLocaleFromFilename(name);
+            // Removes the double quotation mark from the text.
+            result.put(locale, localizedText.substring(1, localizedText.length() - 1));
+        }
+
+        return result;
+    }
+
+    /**
+     * Returns a font object associated given the given locale
+     *
+     * @throws IOException if the font file fails to open
+     * @throws FontFormatException if the font file doesn't have the expected format
+     */
+    private Font loadFontsByLocale(String language) throws IOException, FontFormatException {
+        if (mLoadedFontMap.containsKey(language)) {
+            return mLoadedFontMap.get(language);
+        }
+
+        String fontName = LANGUAGE_TO_FONT_MAP.getOrDefault(language, DEFAULT_FONT_NAME);
+        String[] suffixes = {".otf", ".ttf", ".ttc"};
+        for (String suffix : suffixes) {
+            File fontFile = new File(mFontDirPath, fontName + suffix);
+            if (fontFile.isFile()) {
+                Font result = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize);
+                mLoadedFontMap.put(language, result);
+                return result;
+            }
+        }
+
+        throw new IOException(
+                "Can not find the font file " + fontName + " for language " + language);
+    }
+
+    /** Wraps the text with a maximum of mImageWidth pixels per line. */
+    private WrappedTextInfo wrapText(String text, FontMetrics metrics) {
+        WrappedTextInfo info = new WrappedTextInfo();
+
+        BreakIterator lineBoundary = BreakIterator.getLineInstance();
+        lineBoundary.setText(text);
+
+        int lineWidth = 0;  // Width of the processed words of the current line.
+        int start = lineBoundary.first();
+        StringBuilder line = new StringBuilder();
+        for (int end = lineBoundary.next(); end != BreakIterator.DONE;
+                start = end, end = lineBoundary.next()) {
+            String token = text.substring(start, end);
+            int tokenWidth = metrics.stringWidth(token);
+            // Handles the width mismatch of the word "Android" between different fonts.
+            if (token.contains(ANDROID_STRING)
+                    && metrics.getFont().canDisplayUpTo(ANDROID_STRING) != -1) {
+                tokenWidth = tokenWidth - metrics.stringWidth(ANDROID_STRING) + mAndroidStringWidth;
+            }
+
+            if (lineWidth + tokenWidth > mImageWidth) {
+                info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont);
+
+                line = new StringBuilder();
+                lineWidth = 0;
+            }
+            line.append(token);
+            lineWidth += tokenWidth;
+        }
+
+        info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont);
+
+        return info;
+    }
+
+    /**
+     * Handles the special characters of the raw text embedded in the xml file; and wraps the text
+     * with a maximum of mImageWidth pixels per line.
+     *
+     * @param text the string representation of text to wrap
+     * @param metrics the metrics of the Font used to draw the text; it gives the width in pixels of
+     *     the text given its string representation
+     * @return a WrappedTextInfo class with the width of each AttributedString smaller than
+     *     mImageWidth pixels
+     */
+    private WrappedTextInfo processAndWrapText(String text, FontMetrics metrics) {
+        // Apostrophe is escaped in the xml file.
+        String processed = text.replace("\\'", "'");
+        // The separator "\n\n" indicates a new line in the text.
+        String[] lines = processed.split("\\\\n\\\\n");
+        WrappedTextInfo result = new WrappedTextInfo();
+        for (String line : lines) {
+            result.addLines(wrapText(line, metrics));
+        }
+
+        return result;
+    }
+
+    /**
+     * Encodes the information of the text image for |locale|. According to minui/resources.cpp, the
+     * width, height and locale of the image is decoded as: int w = (row[1] << 8) | row[0]; int h =
+     * (row[3] << 8) | row[2]; __unused int len = row[4]; char* loc =
+     * reinterpret_cast<char*>(&row[5]);
+     */
+    private List<Integer> encodeTextInfo(int width, int height, String locale) {
+        List<Integer> info =
+                new ArrayList<>(
+                        Arrays.asList(
+                                width & 0xff,
+                                width >> 8,
+                                height & 0xff,
+                                height >> 8,
+                                locale.length()));
+
+        byte[] localeBytes = locale.getBytes();
+        for (byte b : localeBytes) {
+            info.add((int) b);
+        }
+        info.add(0);
+
+        return info;
+    }
+
+    /** Returns Graphics2D object that uses the given locale. */
+    private Graphics2D createGraphics(Locale locale) throws IOException, FontFormatException {
+        Graphics2D graphics = mBufferedImage.createGraphics();
+        graphics.setColor(Color.WHITE);
+        graphics.setRenderingHint(
+                RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
+        graphics.setFont(loadFontsByLocale(locale.getLanguage()));
+
+        return graphics;
+    }
+
+    /** Returns the maximum screen width needed to fit the given text after wrapping. */
+    private int measureTextWidth(String text, Locale locale)
+            throws IOException, FontFormatException {
+        Graphics2D graphics = createGraphics(locale);
+        FontMetrics fontMetrics = graphics.getFontMetrics();
+        WrappedTextInfo wrappedTextInfo = processAndWrapText(text, fontMetrics);
+
+        int textWidth = 0;
+        for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) {
+            textWidth = Math.max(textWidth, lineInfo.mLineWidth);
+        }
+
+        // This may happen if one single word is larger than the image width.
+        if (textWidth > mImageWidth) {
+            throw new IllegalStateException(
+                    "Wrapped text width "
+                            + textWidth
+                            + " is larger than image width "
+                            + mImageWidth
+                            + " for locale: "
+                            + locale);
+        }
+
+        return textWidth;
+    }
+
+    /**
+     * Draws the text string on the canvas for given locale.
+     *
+     * @param text the string to draw on canvas
+     * @param locale the current locale tag of the string to draw
+     * @throws IOException if we cannot find the corresponding font file for the given locale.
+     * @throws FontFormatException if we failed to load the font file for the given locale.
+     */
+    private void drawText(String text, Locale locale, String languageTag)
+            throws IOException, FontFormatException {
+        LOGGER.info("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
+
+        Graphics2D graphics = createGraphics(locale);
+        FontMetrics fontMetrics = graphics.getFontMetrics();
+        WrappedTextInfo wrappedTextInfo = processAndWrapText(text, fontMetrics);
+
+        // Marks the start y offset for the text image of current locale; and reserves one line to
+        // encode the image metadata.
+        int currentImageStart = mVerticalOffset;
+        mVerticalOffset += 1;
+        for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) {
+            int lineHeight = fontMetrics.getHeight();
+            // Doubles the height of the image if we are short of space.
+            if (mVerticalOffset + lineHeight >= mImageHeight) {
+                resize(mImageWidth, mImageHeight * 2);
+                // Recreates the graphics since it's attached to the buffered image.
+                graphics = createGraphics(locale);
+            }
+
+            // Draws the text at mVerticalOffset and increments the offset with line space.
+            int baseLine = mVerticalOffset + lineHeight - fontMetrics.getDescent();
+
+            // Draws from right if it's an RTL language.
+            int x =
+                    mCenterAlignment
+                            ? (mImageWidth - lineInfo.mLineWidth) / 2
+                            : RTL_LANGUAGE.contains(languageTag)
+                                    ? mImageWidth - lineInfo.mLineWidth
+                                    : 0;
+            graphics.drawString(lineInfo.mLineContent.getIterator(), x, baseLine);
+
+            mVerticalOffset += lineHeight;
+        }
+
+        // Encodes the metadata of the current localized image as pixels.
+        int currentImageHeight = mVerticalOffset - currentImageStart - 1;
+        List<Integer> info = encodeTextInfo(mImageWidth, currentImageHeight, languageTag);
+        for (int i = 0; i < info.size(); i++) {
+            int[] pixel = {info.get(i)};
+            mBufferedImage.getRaster().setPixel(i, currentImageStart, pixel);
+        }
+    }
+
+    /**
+     * Redraws the image with the new width and new height.
+     *
+     * @param width the new width of the image in pixels.
+     * @param height the new height of the image in pixels.
+     */
+    private void resize(int width, int height) {
+        BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
+        Graphics2D graphic = resizedImage.createGraphics();
+        graphic.drawImage(mBufferedImage, 0, 0, null);
+        graphic.dispose();
+
+        mBufferedImage = resizedImage;
+        mImageWidth = width;
+        mImageHeight = height;
+    }
+
+    /**
+     * This function draws the font characters and saves the result to outputPath.
+     *
+     * @param localizedTextMap a map from locale to its translated text string
+     * @param outputPath the path to write the generated image file.
+     * @throws FontFormatException if there's a format error in one of the font file
+     * @throws IOException if we cannot find the font file for one of the locale, or we failed to
+     *     write the image file.
+     */
+    public void generateImage(Map<Locale, String> localizedTextMap, String outputPath)
+            throws FontFormatException, IOException {
+        FontMetrics defaultFontMetrics =
+                createGraphics(Locale.forLanguageTag("en")).getFontMetrics();
+        mDefaultFont = defaultFontMetrics.getFont();
+        mAndroidStringWidth = defaultFontMetrics.stringWidth(ANDROID_STRING);
+
+        // The last country variant should be the fallback locale for a given language.
+        Map<String, Locale> fallbackLocaleMap = new HashMap<>();
+        int textWidth = 0;
+        for (Locale locale : localizedTextMap.keySet()) {
+            // Updates the fallback locale if we have a new language variant. Don't do it for en-XC
+            // as it's a pseudo-locale.
+            if (!locale.toLanguageTag().equals("en-XC")) {
+                fallbackLocaleMap.put(locale.getLanguage(), locale);
+            }
+            textWidth = Math.max(textWidth, measureTextWidth(localizedTextMap.get(locale), locale));
+        }
+
+        // Removes the black margins to reduce the size of the image.
+        resize(textWidth, mImageHeight);
+
+        for (Locale locale : localizedTextMap.keySet()) {
+            // Recovery expects en-US instead of en_US.
+            String languageTag = locale.toLanguageTag();
+            Locale fallbackLocale = fallbackLocaleMap.get(locale.getLanguage());
+            if (locale.equals(fallbackLocale)) {
+                // Makes the last country variant for a given language be the catch-all for that
+                // language.
+                languageTag = locale.getLanguage();
+            } else if (localizedTextMap.get(locale).equals(localizedTextMap.get(fallbackLocale))) {
+                LOGGER.info("Skip parsing text for duplicate locale " + locale);
+                continue;
+            }
+
+            drawText(localizedTextMap.get(locale), locale, languageTag);
+        }
+
+        resize(mImageWidth, mVerticalOffset);
+        ImageIO.write(mBufferedImage, "png", new File(outputPath));
+    }
+
+    /** Prints the helper message. */
+    public static void printUsage(Options options) {
+        new HelpFormatter().printHelp("java -jar path_to_jar [required_options]", options);
+    }
+
+    /** Creates the command line options. */
+    public static Options createOptions() {
+        Options options = new Options();
+        options.addOption(
+                OptionBuilder.withLongOpt("image_width")
+                        .withDescription("The initial width of the image in pixels.")
+                        .hasArgs(1)
+                        .isRequired()
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("text_name")
+                        .withDescription(
+                                "The description of the text string, e.g. recovery_erasing")
+                        .hasArgs(1)
+                        .isRequired()
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("font_dir")
+                        .withDescription(
+                                "The directory that contains all the support font format files, "
+                                        + "e.g. $OUT/system/fonts/")
+                        .hasArgs(1)
+                        .isRequired()
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("resource_dir")
+                        .withDescription(
+                                "The resource directory that contains all the translated strings in"
+                                        + " xml format, e.g."
+                                        + " bootable/recovery/tools/recovery_l10n/res/")
+                        .hasArgs(1)
+                        .isRequired()
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("output_file")
+                        .withDescription("Path to the generated image.")
+                        .hasArgs(1)
+                        .isRequired()
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("center_alignment")
+                        .withDescription("Align the text in the center of the screen.")
+                        .hasArg(false)
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("verbose")
+                        .withDescription("Output the logging above info level.")
+                        .hasArg(false)
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("locales")
+                        .withDescription("A list of android locales separated by ',' e.g."
+                                + " 'af,en,zh-rTW'")
+                        .hasArg(true)
+                        .create());
+
+        return options;
+    }
+
+    /** The main function parses the command line options and generates the desired text image. */
+    public static void main(String[] args)
+            throws NumberFormatException, IOException, FontFormatException,
+                    LocalizedStringNotFoundException {
+        Options options = createOptions();
+        CommandLine cmd;
+        try {
+            cmd = new GnuParser().parse(options, args);
+        } catch (ParseException e) {
+            System.err.println(e.getMessage());
+            printUsage(options);
+            return;
+        }
+
+        int imageWidth = Integer.parseUnsignedInt(cmd.getOptionValue("image_width"));
+
+        if (cmd.hasOption("verbose")) {
+            LOGGER.setLevel(Level.INFO);
+        } else {
+            LOGGER.setLevel(Level.WARNING);
+        }
+
+        ImageGenerator imageGenerator =
+                new ImageGenerator(
+                        imageWidth,
+                        cmd.getOptionValue("text_name"),
+                        DEFAULT_FONT_SIZE,
+                        cmd.getOptionValue("font_dir"),
+                        cmd.hasOption("center_alignment"));
+
+        Set<String> localesSet = null;
+        if (cmd.hasOption("locales")) {
+            String[] localesList = cmd.getOptionValue("locales").split(",");
+            localesSet = new HashSet<>(Arrays.asList(localesList));
+            // Ensures that we have the default locale, all english translations are identical.
+            localesSet.add("en-rAU");
+        }
+        Map<Locale, String> localizedStringMap =
+                imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"),
+                        localesSet);
+        imageGenerator.generateImage(localizedStringMap, cmd.getOptionValue("output_file"));
+    }
+}
diff --git a/tools/image_generator/ImageGenerator.mf b/tools/image_generator/ImageGenerator.mf
new file mode 100644
index 0000000..17712d1
--- /dev/null
+++ b/tools/image_generator/ImageGenerator.mf
@@ -0,0 +1 @@
+Main-Class: com.android.recovery.tools.ImageGenerator
diff --git a/tools/image_generator/README.md b/tools/image_generator/README.md
new file mode 100644
index 0000000..5d70354
--- /dev/null
+++ b/tools/image_generator/README.md
@@ -0,0 +1,21 @@
+Recovery Image Generator
+-------------------------
+
+This program uses java.awt.Graphics2D to generate the background text files used
+under recovery mode. And thus we don't need to do the manual work by running
+emulators with different dpi.
+
+# Usage:
+  `java -jar path_to_jar --image_width imageWidth --text_name textName --font_dir fontDirectory
+   --resource_dir resourceDirectory --output_file outputFilename`
+
+# Description of the parameters:
+1. `imageWidth`: The number of pixels per line; and the text strings will be
+   wrapped accordingly.
+2. `textName`: The description of the text string, e.g. "recovery_erasing",
+   "recovery_installing_security"
+3. `fontDirectory`: The directory that contains all the support .ttf | .ttc
+   files, e.g. $OUT/system/fonts/
+4. `resourceDirectory`: The resource directory that contains all the translated
+   strings in xml format, e.g. bootable/recovery/tools/recovery_l10n/res/
+5. `outputFilename`: Path to the generated image.
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/tools/recovery_l10n/res/values-af/strings.xml b/tools/recovery_l10n/res/values-af/strings.xml
index b1974da..85a3c90 100644
--- a/tools/recovery_l10n/res/values-af/strings.xml
+++ b/tools/recovery_l10n/res/values-af/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Geen opdrag nie"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Fout!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installeer tans sekuriteitopdatering"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan nie Android-stelsel laai nie. Jou data is dalk korrup. As jy aanhou om hierdie boodskap te kry, sal jy dalk \'n fabrieksterugstelling moet doen en alle gebruikerdata moet uitvee wat op hierdie toestel geberg word."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Probeer weer"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Fabrieksterugstelling"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vee alle gebruikerdata uit?\n\n DIT KAN NIE ONTDOEN WORD NIE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Kanselleer"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-am/strings.xml b/tools/recovery_l10n/res/values-am/strings.xml
index 75c17fb..353f223 100644
--- a/tools/recovery_l10n/res/values-am/strings.xml
+++ b/tools/recovery_l10n/res/values-am/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ምንም ትዕዛዝ የለም"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ስህተት!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"የደህንነት ዝማኔ በመጫን ላይ"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"የAndroid ስርዓትን መጫን አልተቻለም። የእርስዎ ውሂብ የተበላሸ ሊሆን ይችላል። ይህን መልዕክት ማግኘቱን ከቀጠሉ የፋብሪካ ውሂብ ዳግም ማስጀመር ማከናወንና በዚህ መሣሪያ ላይ የተከማቸ ሁሉንም የተጠቃሚ ውሂብ መሰረዝ ሊኖርብዎት ይችላል።"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"እንደገና ሞክር"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"የፋብሪካ ውሂብ ዳግም ማስጀመር"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ሁሉም የተጠቃሚ ውሂብ ይሰረዝ?\n\n ይህ ሊቀለበስ አይችልም!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ይቅር"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ar/strings.xml b/tools/recovery_l10n/res/values-ar/strings.xml
index 601b583..2af36d6 100644
--- a/tools/recovery_l10n/res/values-ar/strings.xml
+++ b/tools/recovery_l10n/res/values-ar/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ليس هناك أي أمر"</string>
     <string name="recovery_error" msgid="5748178989622716736">"خطأ!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"جارٍ تثبيت تحديث الأمان"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"‏يتعذَّر تحميل نظام Android، حيث قد تكون بياناتك تالفة. وإذا استمر ظهور هذه الرسالة، قد يتعيَّن عليك إجراء إعادة الضبط بحسب بيانات المصنع ومحو جميع بيانات المستخدم المُخزَّنة على هذا الجهاز."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"إعادة المحاولة"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"إعادة الضبط بحسب بيانات المصنع"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"هل تريد حجب كل بيانات المستخدم؟\n\n لا يمكن التراجع عن هذا الإجراء."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"إلغاء"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-as/strings.xml b/tools/recovery_l10n/res/values-as/strings.xml
index 2624ceb..33a204d 100644
--- a/tools/recovery_l10n/res/values-as/strings.xml
+++ b/tools/recovery_l10n/res/values-as/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"কোনো আদেশ নাই"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ত্ৰুটি!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"সুৰক্ষা আপডেইট ইনষ্টল কৰি থকা হৈছে"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ছিষ্টেম ল\'ড কৰিব নোৱাৰি। আপোনাৰ ডেটাত কিবা আসোঁৱাহ থকা যেন লাগিছে। আপুনি যদি এই বাৰ্তাটো পায়েই থাকে, আপুনি নিজৰ ডিভাইচটো ফেক্টৰী ডেটা ৰিছেট কৰি সেইটোত থকা ব্যৱহাৰকাৰীৰ সকলো ডেটা মচিব লগা হ\'ব পাৰে।"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"আকৌ চেষ্টা কৰক"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ফেক্টৰী ডেটা ৰিছেট"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ব্যৱহাৰকাৰীৰ সকলো ডেটা মচিবনে?\n\n এইটো কৰাৰ পিছত আনডু কৰিব নোৱাৰি!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"বাতিল কৰক"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-az/strings.xml b/tools/recovery_l10n/res/values-az/strings.xml
index c6765a9..35194c4 100644
--- a/tools/recovery_l10n/res/values-az/strings.xml
+++ b/tools/recovery_l10n/res/values-az/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Əmr yoxdur"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Xəta!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Təhlükəsizlik güncəlləməsi yüklənir"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android sistemi yüklənmir. Datanız zədələnə bilər. Bu mesajı yenə qəbul etsəniz, data zavod sıfırlamasını həyata keçirməli və bu cihazda saxlanmış istifadəçi datasının hamısını silməlisiniz."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Yenidən cəhd edin"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Data zavod sıfırlaması"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Bütün istifadəçi datası silinsin?\n\n BU ƏMƏLİYYATI GERİ QAYTARMAQ MÜMKÜN DEYİL!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ləğv edin"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml b/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml
index c2d8f22..19c6f41 100644
--- a/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml
+++ b/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nema komande"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Greška!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalira se bezbednosno ažuriranje"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Učitavanje Android sistema nije uspelo. Podaci su možda oštećeni. Ako nastavite da dobijate ovu poruku, možda ćete morati da resetujete uređaj na fabrička podešavanja i obrišete sve podatke korisnika koje čuvate na njemu."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Probaj ponovo"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Resetovanje na fabrička podešavanja"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite li da izbrišete sve podatke korisnika?\n\n OVO NE MOŽE DA SE OPOZOVE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Otkaži"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-be/strings.xml b/tools/recovery_l10n/res/values-be/strings.xml
index 7c0954d..ad14fbe 100644
--- a/tools/recovery_l10n/res/values-be/strings.xml
+++ b/tools/recovery_l10n/res/values-be/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Няма каманды"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Памылка"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Усталёўка абнаўлення сістэмы бяспекі"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не ўдалося загрузіць сістэму Android. Магчыма, вашы даныя пашкоджаны. Калі вы зноў убачыце гэта паведамленне, скіньце налады прылады да заводскіх значэнняў і сатрыце ўсе карыстальніцкія даныя, якія на ёй захоўваюцца."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Паўтарыць спробу"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Скінуць да заводскіх налад"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ачысціць усе карыстальніцкія даныя?\n\n ГЭТА ДЗЕЯННЕ НЕЛЬГА АДРАБІЦЬ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Скасаваць"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-bg/strings.xml b/tools/recovery_l10n/res/values-bg/strings.xml
index 9e628a2..e96ff44 100644
--- a/tools/recovery_l10n/res/values-bg/strings.xml
+++ b/tools/recovery_l10n/res/values-bg/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Без команда"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Актуализацията на сигурносттa се инсталира"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Системата Android не може да се зареди. Данните ви може да са повредени. Ако продължите да получавате това съобщение, може да е необходимо да възстановите фабричните настройки и да изтриете всички потребителски данни, съхранени на това устройство."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Нов опит"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Възстановяване на фабричните настройки"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Да се изчистят ли всички потребителски данни?\n\n ТОВА ДЕЙСТВИЕ НЕ МОЖЕ ДА БЪДЕ ОТМЕНЕНО!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Отказ"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-bn/strings.xml b/tools/recovery_l10n/res/values-bn/strings.xml
index 0a481fa..5967bc4 100644
--- a/tools/recovery_l10n/res/values-bn/strings.xml
+++ b/tools/recovery_l10n/res/values-bn/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"কোনো আদেশ নেই"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ত্রুটি!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"নিরাপত্তার আপডেট ইনস্টল করা হচ্ছে"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android সিস্টেম লোড করা যায়নি। আপনার ডেটা হয়ত নষ্ট হয়ে গেছে। যদি এই মেসেজটি আসতেই থাকে তাহলে হয়ত ফ্যাক্টরি ডেটা রিসেট করে এই ডিভাইসে থাকা ব্যবহারকারীর সব ডেটা মুছে ফেলতে হবে।"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"আবার চেষ্টা করুন"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ফ্যাক্টরি ডেটা রিসেট করুন"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ব্যবহারকারীর সব ডেটা মুছে দিতে চান?\n\n এই ডেটা আর ফিরে পাওয়া যাবে না!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"বাতিল করুন"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-bs/strings.xml b/tools/recovery_l10n/res/values-bs/strings.xml
index 412cf02..38f197f 100644
--- a/tools/recovery_l10n/res/values-bs/strings.xml
+++ b/tools/recovery_l10n/res/values-bs/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nema komande"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Greška!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instaliranje sigurnosnog ažuriranja…"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nije moguće učitati Android sistem. Podaci su možda oštećeni. Ako opet primite ovu poruku, možda ćete morati vratiti uređaj na fabričke postavke i izbrisati sve podatke korisnika pohranjene na ovom uređaju."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Pokušaj ponovo"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Vraćanje na fabričke postavke"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Izbrisati sve podatke korisnika?\n\n TA RADNJA SE NE MOŽE PONIŠTITI!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Otkaži"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ca/strings.xml b/tools/recovery_l10n/res/values-ca/strings.xml
index 3f266d2..6b7bec0 100644
--- a/tools/recovery_l10n/res/values-ca/strings.xml
+++ b/tools/recovery_l10n/res/values-ca/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"No hi ha cap ordre"</string>
     <string name="recovery_error" msgid="5748178989622716736">"S\'ha produït un error"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"S\'està instal·lant una actualització de seguretat"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No s\'ha pogut carregar el sistema Android. És possible que les teves dades estiguin malmeses. Si continues veient aquest missatge, pot ser que hagis de restablir les dades de fàbrica i esborrar totes les dades d\'usuari emmagatzemades en aquest dispositiu."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Torna-ho a provar"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restableix les dades de fàbrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vols eliminar totes les dades d\'usuari?\n\n AQUESTA ACCIÓ NO ES POT DESFER."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel·la"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-cs/strings.xml b/tools/recovery_l10n/res/values-cs/strings.xml
index eb436a8..c42dab2 100644
--- a/tools/recovery_l10n/res/values-cs/strings.xml
+++ b/tools/recovery_l10n/res/values-cs/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Žádný příkaz"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Chyba!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalace aktualizace zabezpečení"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Systém Android se nepodařilo načíst. Vaše data jsou možná poškozena. Pokud se tato zpráva bude zobrazovat i nadále, bude nutné vymazat všechna uživatelská data v zařízení a obnovit tovární data."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Zkusit znovu"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Obnovení továrních dat"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vymazat všechna uživatelská data?\n\nTUTO AKCI NELZE VRÁTIT ZPĚT!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Zrušit"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-da/strings.xml b/tools/recovery_l10n/res/values-da/strings.xml
index c6e64a2..814c0df 100644
--- a/tools/recovery_l10n/res/values-da/strings.xml
+++ b/tools/recovery_l10n/res/values-da/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ingen kommando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Fejl!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installerer sikkerhedsopdateringen"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-systemet kan ikke indlæses. Dine data er muligvis beskadigede. Hvis du bliver ved med at få denne meddelelse, er du måske nødt til at udføre en gendannelse af fabriksdata og slette alle brugerdata, der er gemt på denne enhed."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Prøv igen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gendannelse af fabriksdata"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vil du rydde alle brugerdata?\n\n DETTE KAN IKKE FORTRYDES!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuller"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-de/strings.xml b/tools/recovery_l10n/res/values-de/strings.xml
index 6b6726a..80fa971 100644
--- a/tools/recovery_l10n/res/values-de/strings.xml
+++ b/tools/recovery_l10n/res/values-de/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Kein Befehl"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Fehler"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Sicherheitsupdate wird installiert"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-System kann nicht geladen werden. Deine Daten sind eventuell beschädigt. Wenn du diese Nachricht weiterhin erhältst, musst du dein Gerät unter Umständen auf die Werkseinstellungen zurücksetzen und alle darauf gespeicherten Nutzerdaten löschen."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Noch einmal versuchen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Zurücksetzen auf Werkseinstellungen"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Alle Nutzerdaten löschen?\n\n DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Abbrechen"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-el/strings.xml b/tools/recovery_l10n/res/values-el/strings.xml
index 4cb2da5..204ae40 100644
--- a/tools/recovery_l10n/res/values-el/strings.xml
+++ b/tools/recovery_l10n/res/values-el/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Καμία εντολή"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Σφάλμα!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Εγκατάσταση ενημέρωσης ασφαλείας"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Δεν είναι δυνατή η φόρτωση του συστήματος Android. Τα δεδομένα σας μπορεί να είναι κατεστραμμένα. Εάν εξακολουθήσετε να λαμβάνετε αυτό το μήνυμα, μπορεί να χρειαστεί να κάνετε επαναφορά εργοστασιακών ρυθμίσεων και να διαγράψετε όλα τα δεδομένα που έχουν αποθηκευτεί σε αυτήν τη συσκευή."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Δοκιμάστε ξανά"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Επαναφορά εργοστασιακών δεδομένων"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Να διαγραφούν όλα τα δεδομένα χρήστη;\n\n ΔΕΝ ΕΙΝΑΙ ΔΥΝΑΤΗ Η ΑΝΑΙΡΕΣΗ ΑΥΤΗΣ ΤΗΣ ΕΝΕΡΓΕΙΑΣ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ακύρωση"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-en-rAU/strings.xml b/tools/recovery_l10n/res/values-en-rAU/strings.xml
index dc75c23..6451e5b 100644
--- a/tools/recovery_l10n/res/values-en-rAU/strings.xml
+++ b/tools/recovery_l10n/res/values-en-rAU/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot 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."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-en-rCA/strings.xml b/tools/recovery_l10n/res/values-en-rCA/strings.xml
index dc75c23..6451e5b 100644
--- a/tools/recovery_l10n/res/values-en-rCA/strings.xml
+++ b/tools/recovery_l10n/res/values-en-rCA/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot 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."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-en-rGB/strings.xml b/tools/recovery_l10n/res/values-en-rGB/strings.xml
index dc75c23..6451e5b 100644
--- a/tools/recovery_l10n/res/values-en-rGB/strings.xml
+++ b/tools/recovery_l10n/res/values-en-rGB/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot 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."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-en-rIN/strings.xml b/tools/recovery_l10n/res/values-en-rIN/strings.xml
index dc75c23..6451e5b 100644
--- a/tools/recovery_l10n/res/values-en-rIN/strings.xml
+++ b/tools/recovery_l10n/res/values-en-rIN/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot 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."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-en-rXC/strings.xml b/tools/recovery_l10n/res/values-en-rXC/strings.xml
index 3b5cd44..61390f1 100644
--- a/tools/recovery_l10n/res/values-en-rXC/strings.xml
+++ b/tools/recovery_l10n/res/values-en-rXC/strings.xml
@@ -1,9 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <resources xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
-    <string name="recovery_installing" msgid="2013591905463558223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‏‎‎‏‏‏‏‎Installing system update‎‏‎‎‏‎"</string>
-    <string name="recovery_erasing" msgid="7334826894904037088">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‎‏‎‏‎‏‎‎‎‏‎‏‎‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‎Erasing‎‏‎‎‏‎"</string>
-    <string name="recovery_no_command" msgid="4465476568623024327">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‏‎‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎No command‎‏‎‎‏‎"</string>
-    <string name="recovery_error" msgid="5748178989622716736">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‏‏‏‏‏‏‎‎‎‏‎‏‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‏‎‎‎‎‎‎‎Error!‎‏‎‎‏‎"</string>
-    <string name="recovery_installing_security" msgid="9184031299717114342">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎Installing security update‎‏‎‎‏‎"</string>
+    <string name="recovery_installing" msgid="2013591905463558223">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‎‏‏‏‎‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‏‏‏‎‎‏‎‏‎‏‏‎‏‏‎‎‏‏‏‏‏‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‏‎‎‏‏‏‏‎Installing system update‎‏‎‎‏‎"</string>
+    <string name="recovery_erasing" msgid="7334826894904037088">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‏‎‎‏‎‏‎‏‎‎‎‏‎‏‎‎‎‏‏‎‎‏‏‎‎‎‎‎‏‏‏‏‎‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‏‏‎‎‎‎‎‎Erasing‎‏‎‎‏‎"</string>
+    <string name="recovery_no_command" msgid="4465476568623024327">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‎‏‏‏‏‎‏‏‏‏‏‏‎‎‎‏‎‎‎‏‏‏‏‎‏‎‎‎‏‏‏‏‎‏‏‎‎‎‏‏‎‎‏‏‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‎‏‏‏‎No command‎‏‎‎‏‎"</string>
+    <string name="recovery_error" msgid="5748178989622716736">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‏‏‏‏‏‏‎‎‎‏‎‏‏‎‏‎‎‎‏‎‎‏‎‏‎‏‎‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‏‏‎‏‏‎‏‎‎‏‎‏‎‎‎‎‎‎‎Error!‎‏‎‎‏‎"</string>
+    <string name="recovery_installing_security" msgid="9184031299717114342">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‏‎‎‎‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‏‎‎‏‎‏‎‎‏‏‏‏‎‎‏‏‎‎Installing security update‎‏‎‎‏‎"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‎‎‏‏‎‎‎‎‎‏‎‎‏‎‏‎‎‏‎‎‏‏‎‏‏‎‏‎‎‎‎‎‏‎‏‎‎‏‎‎‎‏‏‏‎‎Cannot 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.‎‏‎‎‏‎"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎‎‏‎‎‎‎‏‎‎‏‎‏‏‏‏‎‏‏‎‏‏‎‏‏‏‎‎‎‎‎‏‏‏‎‎‏‏‎‎‎‎Try again‎‏‎‎‏‎"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‎‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‏‏‎‏‎‏‎‏‏‎‏‏‏‏‏‎‎‎‎‎‏‎‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‎Factory data reset‎‏‎‎‏‎"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‏‏‎‎‎‏‎‎‎‏‎‏‎‎‎‎‎‏‎‎‏‏‎‎‏‎‏‎‎‏‎‎‎‏‏‎‏‎‏‎‎‎‎‏‎‏‏‎‎‏‎‎Wipe all user data?‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎‎‏‎‎‏‏‎\n‎‏‎‎‏‏‏‎ THIS CAN NOT BE UNDONE!‎‏‎‎‏‎"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‏‎‎‏‎‎‎‏‎‏‏‏‎‎‎‏‏‏‎‏‏‎‏‏‏‏‏‏‏‎‎‏‏‏‏‎‎‏‎‎‏‏‎‏‏‏‎‏‏‏‎‏‎‏‏‏‎‏‎‎‏‎‎‎‎‎‏‏‎‎‎‎Cancel‎‏‎‎‏‎"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-es-rUS/strings.xml b/tools/recovery_l10n/res/values-es-rUS/strings.xml
index 06b8606..c0baa59 100644
--- a/tools/recovery_l10n/res/values-es-rUS/strings.xml
+++ b/tools/recovery_l10n/res/values-es-rUS/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ningún comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguridad"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No se puede cargar el sistema Android. Es posible que los datos estén dañados. Si este mensaje no desaparece, es posible que debas restablecer la configuración de fábrica del dispositivo y borrar todos los datos del usuario almacenados en él."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Reintentar"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecer configuración de fábrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"¿Quieres borrar todos los datos del usuario?\n\n ESTA ACCIÓN NO SE PUEDE DESHACER"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-es/strings.xml b/tools/recovery_l10n/res/values-es/strings.xml
index d8618f2..de3b69b 100644
--- a/tools/recovery_l10n/res/values-es/strings.xml
+++ b/tools/recovery_l10n/res/values-es/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Sin comandos"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguridad"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No se puede cargar el sistema Android. Es posible que tus datos estén dañados. Si sigue apareciendo este mensaje, es posible que tengas que restablecer el estado de fábrica y borrar todos los datos de usuario almacenados en este dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Reintentar"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecer estado de fábrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"¿Quieres borrar todos los datos de usuario?\n\n ESTA ACCIÓN NO SE PUEDE DESHACER."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-et/strings.xml b/tools/recovery_l10n/res/values-et/strings.xml
index 072a9ef..cafb32f 100644
--- a/tools/recovery_l10n/res/values-et/strings.xml
+++ b/tools/recovery_l10n/res/values-et/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Käsk puudub"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Viga!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Turvavärskenduse installimine"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-süsteemi ei saa laadida. Teie andmed on võib-olla rikutud. Kui jätkate selle sõnumi hankimist, peate võib-olla tegema tehaseandmetele lähtestamise ja kustutama kõik sellesse seadmesse salvestatud kasutajaandmed."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Proovige uuesti"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tehaseandmetele lähtestamine"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Kas kustutada kõik kasutajaandmed?\n\n SEDA TOIMINGUT EI SAA TAGASI VÕTTA!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Tühista"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-eu/strings.xml b/tools/recovery_l10n/res/values-eu/strings.xml
index 5540469..005a042 100644
--- a/tools/recovery_l10n/res/values-eu/strings.xml
+++ b/tools/recovery_l10n/res/values-eu/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ez dago agindurik"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Errorea"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Segurtasun-eguneratzea instalatzen"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ezin da kargatu Android sistema. Zure datuak hondatuta egon daitezke. Mezu hau jasotzen jarraitzen baduzu, jatorrizko datuak berrezarri beharko dituzu eta gailuan gordetako erabiltzaile-datu guztiak ezabatu beharko dituzu."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Saiatu berriro"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Berrezarri jatorrizko datuak"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Erabiltzailearen datu guztiak xahutu nahi dituzu?\n\n EKINTZA HORI EZIN DA DESEGIN!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Utzi"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-fa/strings.xml b/tools/recovery_l10n/res/values-fa/strings.xml
index cc390ae..1c1be9a 100644
--- a/tools/recovery_l10n/res/values-fa/strings.xml
+++ b/tools/recovery_l10n/res/values-fa/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"فرمانی وجود ندارد"</string>
     <string name="recovery_error" msgid="5748178989622716736">"خطا!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"در حال نصب به‌روزرسانی امنیتی"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"‏نمی‌توان سیستم Android را بارگیری کرد. ممکن است داده‌های شما خراب باشند. اگر همچنان این پیام را دریافت می‌کنید، شاید لازم باشد بازنشانی داده‌های کارخانه‌ای انجام دهید و همه داده‌های کاربر را که در این دستگاه ذخیره شده است پاک کنید."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"تلاش مجدد"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"بازنشانی داده‌های کارخانه"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"همه داده‌های کاربر پاک شود؟\n\n این کار قابل‌واگرد نیست!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"لغو"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-fi/strings.xml b/tools/recovery_l10n/res/values-fi/strings.xml
index 5141642..fddaf14 100644
--- a/tools/recovery_l10n/res/values-fi/strings.xml
+++ b/tools/recovery_l10n/res/values-fi/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ei komentoa"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Virhe!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Asennetaan tietoturvapäivitystä"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-järjestelmän lataaminen epäonnistui. Datasi voi olla vioittunut. Jos näet tämän viestin toistuvasti, sinun on ehkä palautettava tehdasasetukset ja poistettava kaikki laitteella olevat käyttäjätiedot."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Yritä uudelleen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tehdasasetuksien palauttaminen"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Poistetaanko kaikki käyttäjätiedot?\n\nTÄTÄ EI VOI PERUA!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Peruuta"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-fr-rCA/strings.xml b/tools/recovery_l10n/res/values-fr-rCA/strings.xml
index b241529..978e9ff 100644
--- a/tools/recovery_l10n/res/values-fr-rCA/strings.xml
+++ b/tools/recovery_l10n/res/values-fr-rCA/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Aucune commande"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erreur!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installation de la mise à jour de sécurité en cours..."</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossible de charger le système Android. Il se peut que vos données soient corrompues. Si vous continuez de recevoir ce message, vous devrez peut-être effectuer une réinitialisation de l\'appareil à ses paramètres d\'usine et effacer toutes les données d\'utilisateur qu\'il contient."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Réessayer"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Réinitialiser aux paramètres d\'usine"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Effacer toutes les données de l\'utilisateur?\n\n CETTE ACTION NE PEUT PAS ÊTRE ANNULÉE!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuler"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-fr/strings.xml b/tools/recovery_l10n/res/values-fr/strings.xml
index f0472b5..693a5dd 100644
--- a/tools/recovery_l10n/res/values-fr/strings.xml
+++ b/tools/recovery_l10n/res/values-fr/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Aucune commande"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erreur !"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installation de la mise à jour de sécurité…"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossible de charger le système Android. Vos données sont peut-être corrompues. Si vous continuez à recevoir ce message, vous devrez peut-être rétablir la configuration d\'usine de votre appareil et effacer toutes les données utilisateur stockées sur cet appareil."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Réessayer"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rétablir la configuration d\'usine"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Effacer toutes les données utilisateur ?\n\n CETTE ACTION NE PEUT PAS ÊTRE ANNULÉE."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuler"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-gl/strings.xml b/tools/recovery_l10n/res/values-gl/strings.xml
index 42b2016..e51b36d 100644
--- a/tools/recovery_l10n/res/values-gl/strings.xml
+++ b/tools/recovery_l10n/res/values-gl/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Non hai ningún comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erro"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguranza"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Non se puido cargar o sistema Android. Os teus datos poden estar danados. Se segue aparecendo esta mensaxe, pode ser necesario restablecer os datos de fábrica e borrar todos os datos de usuario almacenados neste dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Tentar de novo"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecemento dos datos de fábrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Queres borrar todos os datos de usuario?\n\n ESTA ACCIÓN NON SE PODE DESFACER."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-gu/strings.xml b/tools/recovery_l10n/res/values-gu/strings.xml
index 2355a0f..bd83447 100644
--- a/tools/recovery_l10n/res/values-gu/strings.xml
+++ b/tools/recovery_l10n/res/values-gu/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"કોઈ આદેશ નથી"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ભૂલ!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"સુરક્ષા અપડેટ ઇન્સ્ટૉલ કરી રહ્યાં છે"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android સિસ્ટમ લોડ કરી શકાતી નથી. તમારો ડેટા કદાચ દૂષિત થયો હોઈ શકે છે. જો તમને આ સંદેશ મળવાનું ચાલુ રહે, તો કદાચ તમારે આ ડિવાઇસ માટે ફેક્ટરી ડેટા રીસેટ કરવાની પ્રક્રિયા કરવી અને આના પર સ્ટોર કરેલો વપરાશકર્તાનો બધો ડેટા કાઢી નાખવો જરૂરી રહેશે."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ફરી પ્રયાસ કરો"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ફેક્ટરી ડેટા રીસેટ કરો"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"શું વપરાશકર્તાનો બધો ડેટા વાઇપ કરીએ?\n\n આ ક્રિયામાં કરેલો ફેરફાર રદ કરી શકાતો નથી!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"રદ કરો"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-hi/strings.xml b/tools/recovery_l10n/res/values-hi/strings.xml
index 65d0033..c1aa2e9 100644
--- a/tools/recovery_l10n/res/values-hi/strings.xml
+++ b/tools/recovery_l10n/res/values-hi/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"कोई निर्देश नहीं मिला"</string>
     <string name="recovery_error" msgid="5748178989622716736">"गड़बड़ी!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा अपडेट इंस्टॉल किया जा रहा है"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android सिस्टम लोड नहीं किया जा सकता. शायद आपके डेटा में गड़बड़ी है. अगर आपको यह मैसेज मिलता रहता है, तो शायद आपको फ़ैक्ट्री डेटा रीसेट करना पड़े और इस डिवाइस की मेमोरी में मौजूद उपयोगकर्ता का सभी डेटा हमेशा के लिए मिटाना पड़े."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"फिर से कोशिश करें"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फ़ैक्ट्री डेटा रीसेट"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"क्या उपयोगकर्ता का सभी डेटा मिटाएं?\n\n इसे वापस नहीं लाया जा सकता!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"अभी नहीं"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-hr/strings.xml b/tools/recovery_l10n/res/values-hr/strings.xml
index 3b75ff1..0fa8fa9 100644
--- a/tools/recovery_l10n/res/values-hr/strings.xml
+++ b/tools/recovery_l10n/res/values-hr/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nema naredbe"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Pogreška!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instaliranje sigurnosnog ažuriranja"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sustav Android ne može se učitati. Podaci su možda oštećeni. Ako opet primite ovu poruku, možda ćete morati vratiti uređaj na tvorničko stanje i izbrisati sve podatke korisnika pohranjene na ovom uređaju."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Pokušaj ponovo"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Vraćanje na tvorničko stanje"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite li izbrisati sve podatke korisnika?\n\n TO SE NE MOŽE PONIŠTITI!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Odustani"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-hu/strings.xml b/tools/recovery_l10n/res/values-hu/strings.xml
index 12d4d9f..b7998ce 100644
--- a/tools/recovery_l10n/res/values-hu/strings.xml
+++ b/tools/recovery_l10n/res/values-hu/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nincs parancs"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Hiba!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Biztonsági frissítés telepítése"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nem sikerült az Android rendszer betöltése. Az adatok sérültek lehetnek. Ha újra megjelenik ez az üzenet, előfordulhat, hogy vissza kell állítania az eszköz gyári adatait, és törölnie kell az eszközön tárolt összes felhasználói adatot."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Újra"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gyári adatok visszaállítása"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Törli az összes felhasználói adatot?\n\n A MŰVELET NEM VONHATÓ VISSZA."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Mégse"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-hy/strings.xml b/tools/recovery_l10n/res/values-hy/strings.xml
index 9d62bb7..35a0ab1 100644
--- a/tools/recovery_l10n/res/values-hy/strings.xml
+++ b/tools/recovery_l10n/res/values-hy/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Հրամանը տրված չէ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Սխալ"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Անվտանգության թարմացման տեղադրում"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Չհաջողվեց բեռնել Android համակարգը։ Հնարավոր է՝ ձեր տվյալները վնասված են։ Եթե նորից տեսնեք այս հաղորդագրությունը, փորձեք վերակայել սարքի կարգավորումները և ջնջել օգտատիրոջ բոլոր տվյալները։"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Նորից փորձել"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Վերակայել բոլոր տվյալները"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Մաքրե՞լ օգտատիրոջ բոլոր տվյալները։\n\n ԱՅՍ ԳՈՐԾՈՂՈՒԹՅՈՒՆԸ ՀՆԱՐԱՎՈՐ ՉԻ ԼԻՆԻ ՀԵՏԱՐԿԵԼ"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Չեղարկել"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-in/strings.xml b/tools/recovery_l10n/res/values-in/strings.xml
index 0e56e0d..15a78ec 100644
--- a/tools/recovery_l10n/res/values-in/strings.xml
+++ b/tools/recovery_l10n/res/values-in/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Tidak ada perintah"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Memasang pembaruan keamanan"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Tidak dapat memuat sistem Android. Data Anda mungkin rusak. Jika terus mendapatkan pesan ini, Anda mungkin perlu melakukan reset ke setelan pabrik dan menghapus semua data pengguna yang disimpan di perangkat ini."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Coba lagi"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Reset ke setelan pabrik"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe semua data pengguna?\n\n TINDAKAN INI TIDAK DAPAT DIURUNGKAN!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Batal"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-is/strings.xml b/tools/recovery_l10n/res/values-is/strings.xml
index 5065b65..4a6295a 100644
--- a/tools/recovery_l10n/res/values-is/strings.xml
+++ b/tools/recovery_l10n/res/values-is/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Engin skipun"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Villa!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Setur upp öryggisuppfærslu"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ekki er hægt að hlaða Android kerfi. Gögnin þín kunna að vera skemmd. Ef þessi skilaboð halda áfram að birtast gætirðu þurft að núllstilla og eyða öllum notandagögnum sem eru vistuð í þessu tæki."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Reyna aftur"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Núllstilling"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Viltu eyða öllum notandagögnum?\n\n EKKI ER HÆGT AÐ AFTURKALLA ÞETTA!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Hætta við"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-it/strings.xml b/tools/recovery_l10n/res/values-it/strings.xml
index 2c0364e..8bc203c 100644
--- a/tools/recovery_l10n/res/values-it/strings.xml
+++ b/tools/recovery_l10n/res/values-it/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nessun comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Errore!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installazione aggiornamento sicurezza…"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossibile caricare il sistema Android. I tuoi dati potrebbero essere danneggiati. Se continui a ricevere questo messaggio, potrebbe essere necessario eseguire un ripristino dei dati di fabbrica e cancellare tutti i dati utente memorizzati su questo dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Riprova"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ripristino dati di fabbrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vuoi cancellare tutti i dati utente?\n\n NON È POSSIBILE ANNULLARE L\'OPERAZIONE."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annulla"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-iw/strings.xml b/tools/recovery_l10n/res/values-iw/strings.xml
index ea5e6f2..8ca3bdf 100644
--- a/tools/recovery_l10n/res/values-iw/strings.xml
+++ b/tools/recovery_l10n/res/values-iw/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"אין פקודה"</string>
     <string name="recovery_error" msgid="5748178989622716736">"שגיאה!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"מתקין עדכון אבטחה"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"‏לא ניתן לטעון את מערכת Android. ייתכן שהנתונים שלך פגומים. אם הודעה זו תופיע שוב, ייתכן שיהיה עליך לבצע איפוס לנתוני היצרן ולמחוק את כל נתוני המשתמש ששמורים במכשיר זה."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ניסיון נוסף"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"איפוס לנתוני היצרן"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"לאפס את כל נתוני המשתמש?\n\n לא ניתן לבטל פעולה זו!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ביטול"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ja/strings.xml b/tools/recovery_l10n/res/values-ja/strings.xml
index 36e029b..3d66372 100644
--- a/tools/recovery_l10n/res/values-ja/strings.xml
+++ b/tools/recovery_l10n/res/values-ja/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"コマンドが指定されていません"</string>
     <string name="recovery_error" msgid="5748178989622716736">"エラーが発生しました。"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"セキュリティ アップデートをインストールしています"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android システムを読み込めません。データが破損している可能性があります。このメッセージが引き続き表示される場合は、データの初期化を行い、この端末に保存されているすべてのユーザー データを消去することが必要な場合があります。"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"再試行"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"データの初期化"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"すべてのユーザー データをワイプしますか?\n\nこの操作は元に戻せません。"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"キャンセル"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ka/strings.xml b/tools/recovery_l10n/res/values-ka/strings.xml
index 6a46b36..04b8a41 100644
--- a/tools/recovery_l10n/res/values-ka/strings.xml
+++ b/tools/recovery_l10n/res/values-ka/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ბრძანება არ არის"</string>
     <string name="recovery_error" msgid="5748178989622716736">"წარმოიქმნა შეცდომა!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"მიმდინარეობს უსაფრთხოების განახლების ინსტალაცია"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android სისტემის ჩატვირთვა ვერ მოხერხდა. შესაძლოა თქვენი მონაცემები დაზიანებულია. თუ ამ შეტყობინებას კვლავ მიიღებთ, შეიძლება საჭირო იყოს ქარხნული მონაცემების აღდგენა და ამ მოწყობილობაზე შენახული მომხმარებლის ყველა მონაცემის ამოშლა."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ხელახლა ცდა"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ქარხნული მონაცემების აღდგენა"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"გსურთ მომხმარებლის ყველა მონაცემის ამოშლა?\n\n ამ მოქმედების გაუქმება ვერ მოხერხდება!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"გაუქმება"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-kk/strings.xml b/tools/recovery_l10n/res/values-kk/strings.xml
index a4bd86e..3f6aa23 100644
--- a/tools/recovery_l10n/res/values-kk/strings.xml
+++ b/tools/recovery_l10n/res/values-kk/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Пәрмен жоқ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Қате!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Қауіпсіздік жаңартуы орнатылуда"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android жүйесі жүктелмейді. Деректеріңіз бүлінген болуы мүмкін. Егер осы хабар қайта шықса, зауыттық деректерді қалпына келтіріп, пайдаланушы деректерін жойып көріңіз."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Қайталау"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Зауыттық деректерді қалпына келтіру"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Пайдаланушының барлық деректері жойылсын ба?\n\n БҰЛ ӘРЕКЕТТІ ҚАЙТАРЫЛМАЙДЫ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Бас тарту"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-km/strings.xml b/tools/recovery_l10n/res/values-km/strings.xml
index 313c0f4..0cedb6b 100644
--- a/tools/recovery_l10n/res/values-km/strings.xml
+++ b/tools/recovery_l10n/res/values-km/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"គ្មានពាក្យបញ្ជាទេ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"កំហុស!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"កំពុងដំឡើងការអាប់ដេតសុវត្ថិភាព"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"មិនអាច​ផ្ទុកប្រព័ន្ធ Android បានទេ។ ទិន្នន័យ​របស់​អ្នកអាច​នឹងខូច។ ប្រសិនបើ​អ្នក​បន្តទទួល​បានសារនេះ អ្នកអាចនឹងត្រូវកំណត់​ទិន្នន័យ​ដូច​ចេញ​ពី​រោងចក្រ និងលុបទិន្នន័យ​ទាំងអស់​របស់អ្នក​ប្រើប្រាស់​ដែលបានផ្ទុកនៅ​លើ​ឧបករណ៍​នេះ។"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ព្យាយាម​ម្ដងទៀត"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"កំណត់​ទិន្នន័យ​ដូច​ចេញ​ពី​រោងចក្រ"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ឈូស​ទិន្នន័យ​ទាំងអស់​របស់អ្នក​ប្រើប្រាស់?\n\nសកម្មភាព​នេះមិនអាចត្រឡប់វិញបានទេ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"បោះបង់"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-kn/strings.xml b/tools/recovery_l10n/res/values-kn/strings.xml
index 5bf6260..a98f469 100644
--- a/tools/recovery_l10n/res/values-kn/strings.xml
+++ b/tools/recovery_l10n/res/values-kn/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ಯಾವುದೇ ಆದೇಶವಿಲ್ಲ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ದೋಷ!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"ಭದ್ರತೆಯ ಅಪ್‌ಡೇಟ್‌ ಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ಸಿಸ್ಟಂ ಅನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನಿಮ್ಮ ಡೇಟಾ ದೋಷಪೂರಿತವಾಗಿರಬಹುದು. ನೀವು ಈ ಸಂದೇಶ ಪಡೆಯುವುದು ಮುಂದುವರಿದರೆ, ನೀವು ಫ್ಯಾಕ್ಟರಿ ಡೇಟಾ ರಿಸೆಟ್ ಮಾಡುವ ಅಗತ್ಯವಿದೆ ಮತ್ತು ಈ ಸಾಧನದಲ್ಲಿ ಸಂಗ್ರಹಿಸಲಾದ ಎಲ್ಲಾ ಬಳಕೆದಾರರ ಡೇಟಾವನ್ನು ಅಳಿಸಬೇಕಾಗುತ್ತದೆ."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ಫ್ಯಾಕ್ಟರಿ ಡೇಟಾ ರಿಸೆಟ್‌"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ಎಲ್ಲಾ ಬಳಕೆದಾರರ ಡೇಟಾವನ್ನು ಅಳಿಸುವುದೇ?\n\n ಇದನ್ನು ರದ್ದುಗೊಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ರದ್ದುಮಾಡಿ"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ko/strings.xml b/tools/recovery_l10n/res/values-ko/strings.xml
index aca13bb..9067f4c 100644
--- a/tools/recovery_l10n/res/values-ko/strings.xml
+++ b/tools/recovery_l10n/res/values-ko/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"명령어 없음"</string>
     <string name="recovery_error" msgid="5748178989622716736">"오류!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"보안 업데이트 설치 중"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android 시스템을 로드할 수 없습니다. 데이터가 손상되었을 수 있습니다. 이 메시지가 계속 표시되면 초기화를 실행하여 기기에 저장된 사용자 데이터를 모두 삭제해야 할 수도 있습니다."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"다시 시도"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"초기화"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"사용자 데이터를 모두 삭제하시겠습니까?\n\n 이 작업은 실행취소할 수 없습니다."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"취소"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ky/strings.xml b/tools/recovery_l10n/res/values-ky/strings.xml
index 0a6bd78..1cd69ea 100644
--- a/tools/recovery_l10n/res/values-ky/strings.xml
+++ b/tools/recovery_l10n/res/values-ky/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Буйрук берилген жок"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Ката!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Коопсуздук жаңыртуусу орнотулууда"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android тутуму жүктөлбөй жатат. Дайындарыңыз бузук болушу мүмкүн. Бул билдирүү дагы деле келе берсе, түзмөктү кайра башынан жөндөп, анда сакталган бардык колдонуучу дайындарын тазалашыңыз керек."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Кайталоо"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Кайра башынан жөндөө"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Колдонуучу дайындарынын баары жашырылсынбы?\n\n МУНУ АРТКА КАЙТАРУУ МҮМКҮН ЭМЕС!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Жок"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-lo/strings.xml b/tools/recovery_l10n/res/values-lo/strings.xml
index d3dbb39..4a81427 100644
--- a/tools/recovery_l10n/res/values-lo/strings.xml
+++ b/tools/recovery_l10n/res/values-lo/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ບໍ່ມີຄຳສັ່ງ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ຜິດພາດ!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"ກຳລັງຕິດຕັ້ງອັບເດດຄວາມປອດໄພ"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"ບໍ່ສາມາດໂຫຼດລະບົບ Android ໄດ້. ຂໍ້ມູນຂອງທ່ານອາດເສຍຫາຍ. ຫາກທ່ານຍັງໄດ້ຮັບຂໍ້ຄວາມນີ້ຕໍ່ໄປ, ທ່ານອາດຕ້ອງຣີເຊັດເປັນຄ່າຈາກໂຮງງານ ແລະ ລຶບຂໍ້ມູນຜູ້ໃຊ້ທັງໝົດທີ່ຈັດເກັບໄວ້ຢູ່ອຸປະກອນນີ້ອອກ."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ລອງໃໝ່"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ຣີເຊັດຄ່າຈາກໂຮງງານ"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ລຶບລ້າງຂໍ້ມູນຜູ້ໃຊ້ທັງໝົດບໍ?\n\n ຄຳສັ່ງນີ້ຈະບໍ່ສາມາດຍົກເລີກໄດ້!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ຍົກເລີກ"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-lt/strings.xml b/tools/recovery_l10n/res/values-lt/strings.xml
index d5d5e88..f9b7d39 100644
--- a/tools/recovery_l10n/res/values-lt/strings.xml
+++ b/tools/recovery_l10n/res/values-lt/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nėra jokių komandų"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Klaida!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Diegiamas saugos naujinys"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Negalima įkelti „Android“ sistemos. Duomenys gali būti pažeisti. Jei ir toliau gausite šį pranešimą, jums gali reikėti atkurti gamyklinius duomenis ir ištrinti visus naudotojo duomenis, saugomus šiame įrenginyje."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Bandyti dar kartą"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gamyklinių duomenų atkūrimas"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Išvalyti visus naudotojo duomenis?\n\n ŠIO VEIKSMO NEGALIMA ANULIUOTI!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Atšaukti"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-lv/strings.xml b/tools/recovery_l10n/res/values-lv/strings.xml
index d877f6a..6cf8ce3 100644
--- a/tools/recovery_l10n/res/values-lv/strings.xml
+++ b/tools/recovery_l10n/res/values-lv/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nav nevienas komandas"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Kļūda!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Notiek drošības atjauninājuma instalēšana"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nevar ielādēt Android sistēmu. Jūsu dati var būt bojāti. Ja šis ziņojums tiek rādīts atkārtoti, iespējams, jums ir jāveic rūpnīcas datu atiestatīšana un jādzēš visi šajā ierīcē saglabātie lietotāja dati."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Mēģināt vēlreiz"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rūpnīcas datu atiestatīšana"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vai dzēst visus lietotāja datus?\n\n ŠO DARBĪBU NEVAR ATSAUKT!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Atcelt"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-mk/strings.xml b/tools/recovery_l10n/res/values-mk/strings.xml
index 3514597..ff56131 100644
--- a/tools/recovery_l10n/res/values-mk/strings.xml
+++ b/tools/recovery_l10n/res/values-mk/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Нема наредба"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Се инсталира безбедносно ажурирање"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не може да се вчита системот Android. Можно е податоците да се оштетени. Ако и понатаму ја примате поракава, можеби ќе треба да извршите ресетирање на фабрички податоци и да ги избришете сите кориснички податоци меморирани на уредов."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Обиди се пак"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ресетирање на фабрички податоци"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Да се избришат ли сите кориснички податоци?\n\n ОВА НЕ МОЖЕ ДА СЕ ВРАТИ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Откажи"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ml/strings.xml b/tools/recovery_l10n/res/values-ml/strings.xml
index b506e25..2b331ac 100644
--- a/tools/recovery_l10n/res/values-ml/strings.xml
+++ b/tools/recovery_l10n/res/values-ml/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"കമാൻഡ് ഒന്നുമില്ല"</string>
     <string name="recovery_error" msgid="5748178989622716736">"പിശക്!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"സുരക്ഷാ അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android സിസ്‌റ്റം ലോഡ് ചെയ്യാനാവില്ല. നിങ്ങളുടെ ഡാറ്റ കേടായിരിക്കാം. ഈ സന്ദേശം തുടർന്നും ലഭിക്കുകയാണെങ്കിൽ, നിങ്ങൾ ഒരു ഫാക്‌ടറി ഡാറ്റ പുനഃക്രമീകരണം നടത്തേണ്ടതുണ്ട് ഒപ്പം ഈ ഉപകരണത്തിൽ സ്‌റ്റോർ ചെയ്‌തിട്ടുള്ള എല്ലാ ഉപയോക്തൃ ഡാറ്റകളും മായ്‌ക്കേണ്ടതുണ്ട്."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"വീണ്ടും ശ്രമിക്കുക"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ഫാക്‌ടറി ഡാറ്റ പുനഃക്രമീകരണം"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"എല്ലാ ഉപയോക്തൃ ഡാറ്റകളും മായ്‌ക്കണോ?\n\n ഇത് പഴയപടിയാക്കാനാവില്ല!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"റദ്ദാക്കുക"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-mn/strings.xml b/tools/recovery_l10n/res/values-mn/strings.xml
index e3dd2e9..b0a57ed 100644
--- a/tools/recovery_l10n/res/values-mn/strings.xml
+++ b/tools/recovery_l10n/res/values-mn/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Тушаал байхгүй"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Алдаа!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Аюулгүй байдлын шинэчлэлтийг суулгаж байна"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Андройд системийг ачаалах боломжгүй байна. Таны өгөгдөл эвдэрч болзошгүй. Хэрэв та энэ мессежийг үргэлжлүүлэн авах бол үйлдвэрээс гарсан төлөвийг ажиллуулж, энэ төхөөрөмжид хадгалсан хэрэглэгчийн бүх өгөгдлийг устгах шаардлагатай байж болзошгүй."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Дахин оролдох"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Үйлдвэрээс гарсан төлөвт"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Хэрэглэгчийн бүх өгөгдлийг арчих уу?\n\n ҮҮНИЙГ БУЦААХ БОЛОМЖГҮЙ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Цуцлах"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-mr/strings.xml b/tools/recovery_l10n/res/values-mr/strings.xml
index 5f82033..9b13707 100644
--- a/tools/recovery_l10n/res/values-mr/strings.xml
+++ b/tools/recovery_l10n/res/values-mr/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"कोणतीही कमांड नाही"</string>
     <string name="recovery_error" msgid="5748178989622716736">"एरर!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा अपडेट इंस्टॉल करत आहे"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android सिस्टम लोड करू शकत नाही. तुमचा डेटा धोक्यात असू शकतो.तुम्हाला हा मेसेज मिळत राहिल्यास, फॅक्टरी डेटा रीसेट करणे आणि या डिव्हाइसवर स्टोअर केलेला सर्व वापरकर्ता डेटा मिटवणे आवश्यक आहे."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"पुन्हा प्रयत्न करा"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फॅक्‍टरी डेटा रीसेट"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"सर्व वापरकर्ता डेटा पुसून टाकायचा का?\n\n हे पहिल्‍यासारखे करू शकत नाही!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"रद्द करा"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ms/strings.xml b/tools/recovery_l10n/res/values-ms/strings.xml
index 0e24ac4..d094f54 100644
--- a/tools/recovery_l10n/res/values-ms/strings.xml
+++ b/tools/recovery_l10n/res/values-ms/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Tiada perintah"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Ralat!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Memasang kemas kini keselamatan"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Tidak dapat memuatkan sistem Android. Data anda mungkin rosak. Jika anda menerima mesej ini secara berterusan, anda mungkin perlu melaksanakan tetapan semula data kilang dan memadamkan semua data pengguna yang disimpan pada peranti ini."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Cuba lagi"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tetapan semula data kilang"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Lapkan semua data pengguna?\n\n TINDAKAN INI TIDAK BOLEH DIBUAT ASAL!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Batal"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-my/strings.xml b/tools/recovery_l10n/res/values-my/strings.xml
index f137524..09cd4ea 100644
--- a/tools/recovery_l10n/res/values-my/strings.xml
+++ b/tools/recovery_l10n/res/values-my/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ညွှန်ကြားချက်မပေးထားပါ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"မှားနေပါသည်!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"လုံခြုံရေး အပ်ဒိတ်ကို ထည့်သွင်းနေသည်"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android စနစ် ဖွင့်၍မရပါ။ သင့်ဒေတာများ ပျက်နေခြင်း ဖြစ်နိုင်သည်။ ဤမက်ဆေ့ဂျ် ဆက်လက်ရရှိနေလျှင် စက်ရုံထုတ်အခြေအနေပြန်ယူပြီး ဤစက်ပေါ်တွင် သိမ်းထားသော အသုံးပြုသူဒေတာအားလုံး ဖျက်ရန် လိုအပ်နိုင်သည်။"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ထပ်စမ်းကြည့်ပါ"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"စက်ရုံထုတ်အခြေအနေပြန်ယူခြင်း"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"အသုံးပြုသူဒေတာ အားလုံးကို ရှင်းလင်းမလား။\n\n ၎င်းကို ပြန်ပြင်၍မရပါ။"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"မလုပ်တော့"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-nb/strings.xml b/tools/recovery_l10n/res/values-nb/strings.xml
index ad6f20e..e8cad13 100644
--- a/tools/recovery_l10n/res/values-nb/strings.xml
+++ b/tools/recovery_l10n/res/values-nb/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ingen kommandoer"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Feil!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Installerer sikkerhetsoppdateringen"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan ikke laste inn Android-systemet. Dataene dine er muligens skadet. Hvis du fortsetter å se denne meldingen, må du muligens tilbakestille til fabrikkstandard og tømme alle brukerdataene som er lagret på denne enheten."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Prøv igjen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tilbakestill til fabrikkstandard"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vil du viske ut alle brukerdataene?\n\n DETTE KAN IKKE ANGRES!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Avbryt"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ne/strings.xml b/tools/recovery_l10n/res/values-ne/strings.xml
index 1880e80..fa53e9d 100644
--- a/tools/recovery_l10n/res/values-ne/strings.xml
+++ b/tools/recovery_l10n/res/values-ne/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"कुनै आदेश छैन"</string>
     <string name="recovery_error" msgid="5748178989622716736">"त्रुटि!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा सम्बन्धी अद्यावधिकलाई स्थापना गर्दै"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android प्रणाली लोड गर्न सकिएन। तपाईंको डेटा बिग्रेको हुन सक्छ। तपाईं यो सन्देश प्राप्त गर्नुहुन्छ भने तपाईंले फ्याक्ट्री डेटा रिसेट गर्न आवश्यक छ र यो यन्त्रमा भण्डारण गरेका सबै प्रयोगकर्ताको डेटा मेट्न पर्छ।"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"फेरि प्रयास गर्नुहोस्"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फ्याक्ट्री डेटा रिसेट"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"प्रयोगकर्ताको सबै डेटा मेट्ने हो?\n\n यो अन्डू गर्न सकिँदैन!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"रद्द गर्नुहोस्"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-nl/strings.xml b/tools/recovery_l10n/res/values-nl/strings.xml
index 0d6c15a..b42bb65 100644
--- a/tools/recovery_l10n/res/values-nl/strings.xml
+++ b/tools/recovery_l10n/res/values-nl/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Geen opdracht"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Fout!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Beveiligingsupdate installeren"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan het Android-systeem niet laden. Je gegevens zijn mogelijk beschadigd. Als je dit bericht blijft ontvangen, moet je mogelijk de fabrieksinstellingen terugzetten en alle gebruikersgegevens wissen die op dit apparaat zijn opgeslagen."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Opnieuw proberen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Terugzetten op fabrieksinstellingen"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Alle gebruikersgegevens wissen?\n\n DIT KAN NIET ONGEDAAN WORDEN GEMAAKT."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuleren"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-or/strings.xml b/tools/recovery_l10n/res/values-or/strings.xml
index 2b0851c..25b28e6 100644
--- a/tools/recovery_l10n/res/values-or/strings.xml
+++ b/tools/recovery_l10n/res/values-or/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"କୌଣସି କମାଣ୍ଡ ନାହିଁ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ତ୍ରୁଟି!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"ସୁରକ୍ଷା ଅପ୍‌ଡେଟ୍‌ ଇନ୍‌ଷ୍ଟଲ୍‌ କରୁଛି"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ସିଷ୍ଟମ୍‍ ଲୋଡ୍‍ କରାଯାଇପାରିବ ନାହିଁ। ଆପଣଙ୍କ ଡାଟା ହୁଏତ ତ୍ରୁଟି ରହିଥାଇ ପାରେ। ଯଦି ଆପଣ ଏହି ମେସେଜ୍‍ ପାଇବା ଜାରି ରଖନ୍ତି, ତେବେ ଆପଣଙ୍କୁ ଫ୍ୟାକ୍ଟେରୀ ଡାଟା ରିସେଟ୍‍ କରିବାକୁ ହେବ ଏବଂ ଏହି ଡିଭାଇସ୍‍‍ରେ ଷ୍ଟୋର୍‍ ହୋଇଥିବା ସମସ୍ତ ଡାଟା ଇରେଜ୍‍ କରନ୍ତୁ।"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ଫ୍ୟାକ୍ଟୋରୀ ଡାଟା ରିସେଟ୍‌"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ସମସ୍ଯ ଉପଯୋଗକର୍ତ୍ତା ଡାଟା ୱାଇପ୍‍ କରିବେ?\n\n ଏହା ଫେରାଇ ନିଆଯାଇପାରିବ ନାହିଁ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ବାତିଲ୍‌ କରନ୍ତୁ"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-pa/strings.xml b/tools/recovery_l10n/res/values-pa/strings.xml
index 27972d1..3743068 100644
--- a/tools/recovery_l10n/res/values-pa/strings.xml
+++ b/tools/recovery_l10n/res/values-pa/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ਕੋਈ ਆਦੇਸ਼ ਨਹੀਂ"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ਅਸ਼ੁੱਧੀ!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"ਸੁਰੱਖਿਆ ਅੱਪਡੇਟ ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ਸਿਸਟਮ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ। ਸ਼ਾਇਦ ਤੁਹਾਡਾ ਡਾਟਾ ਖਰਾਬ ਹੈ। ਜੇਕਰ ਤੁਹਾਨੂੰ ਇਹ ਸੁਨੇਹਾ ਪ੍ਰਾਪਤ ਹੋਣਾ ਜਾਰੀ ਰਹਿੰਦਾ ਹੈ, ਤਾਂ ਸ਼ਾਇਦ ਤੁਹਾਨੂੰ ਫੈਕਟਰੀ ਡਾਟਾ ਰੀਸੈੱਟ ਕਰਨਾ ਪਵੇ ਅਤੇ ਇਸ ਡੀਵਾਈਸ \'ਤੇ ਸਟੋਰ ਕੀਤੇ ਸਾਰੇ ਵਰਤੋਂਕਾਰ ਡਾਟੇ ਨੂੰ ਮਿਟਾਉਣਾ ਪਵੇ।"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ਫੈਕਟਰੀ ਡਾਟਾ ਰੀਸੈੱਟ ਕਰੋ"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ਕੀ ਸਾਰਾ ਵਰਤੋਂਕਾਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨਾ ਹੈ?\n\n ਇਸਨੂੰ ਅਣਕੀਤਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ਰੱਦ ਕਰੋ"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-pl/strings.xml b/tools/recovery_l10n/res/values-pl/strings.xml
index 8d6db38..48d3dbf 100644
--- a/tools/recovery_l10n/res/values-pl/strings.xml
+++ b/tools/recovery_l10n/res/values-pl/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Brak polecenia"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Błąd"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instaluję aktualizację zabezpieczeń"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nie można załadować systemu Android. Dane mogą być uszkodzone. Jeśli ten komunikat nadal będzie się pojawiać, może być konieczne przywrócenie danych fabrycznych urządzenia i usunięcie wszystkich zapisanych na nim danych użytkownika."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Ponów próbę"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Przywracanie danych fabrycznych"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wyczyścić wszystkie dane użytkownika?\n\n TEJ CZYNNOŚCI NIE MOŻNA COFNĄĆ."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anuluj"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-pt-rBR/strings.xml b/tools/recovery_l10n/res/values-pt-rBR/strings.xml
index b727043..0df3edc 100644
--- a/tools/recovery_l10n/res/values-pt-rBR/strings.xml
+++ b/tools/recovery_l10n/res/values-pt-rBR/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando atualização de segurança"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Seus dados podem estar corrompidos. Se você continuar recebendo esta mensagem, talvez seja necessário realizar uma redefinição para a configuração original e limpar todos os dados do usuário armazenados neste dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Redefinição para configuração original"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Limpar todos os dados do usuário?\n\n NÃO É POSSÍVEL DESFAZER ESSA AÇÃO."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-pt-rPT/strings.xml b/tools/recovery_l10n/res/values-pt-rPT/strings.xml
index 9814637..08eb3c9 100644
--- a/tools/recovery_l10n/res/values-pt-rPT/strings.xml
+++ b/tools/recovery_l10n/res/values-pt-rPT/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"A instalar atualização de segurança"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Os seus dados podem estar danificados. Se continuar a receber esta mensagem, pode ter de efetuar uma reposição de dados de fábrica e apagar todos os dados do utilizador armazenados neste dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Reposição de dados de fábrica"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Pretende limpar todos os dados do utilizador?\n\n NÃO É POSSÍVEL ANULAR ESTA AÇÃO."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-pt/strings.xml b/tools/recovery_l10n/res/values-pt/strings.xml
index b727043..0df3edc 100644
--- a/tools/recovery_l10n/res/values-pt/strings.xml
+++ b/tools/recovery_l10n/res/values-pt/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando atualização de segurança"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Seus dados podem estar corrompidos. Se você continuar recebendo esta mensagem, talvez seja necessário realizar uma redefinição para a configuração original e limpar todos os dados do usuário armazenados neste dispositivo."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Redefinição para configuração original"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Limpar todos os dados do usuário?\n\n NÃO É POSSÍVEL DESFAZER ESSA AÇÃO."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ro/strings.xml b/tools/recovery_l10n/res/values-ro/strings.xml
index 8032865..585db83 100644
--- a/tools/recovery_l10n/res/values-ro/strings.xml
+++ b/tools/recovery_l10n/res/values-ro/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nicio comandă"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Eroare!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Se instalează actualizarea de securitate"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nu se poate încărca sistemul Android. Datele dvs. pot fi corupte. Dacă primiți în continuare acest mesaj, poate fi necesar să reveniți la setările din fabrică și să ștergeți toate datele utilizatorului stocate pe acest dispozitiv."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Reîncercați"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Revenire la setările din fabrică"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ștergeți toate datele utilizatorului?\n\n ACEST LUCRU NU POATE FI ANULAT!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anulați"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ru/strings.xml b/tools/recovery_l10n/res/values-ru/strings.xml
index feebecf..db8b761 100644
--- a/tools/recovery_l10n/res/values-ru/strings.xml
+++ b/tools/recovery_l10n/res/values-ru/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Команды нет"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Ошибка"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Установка обновления системы безопасности…"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не удалось загрузить систему Android. Возможно, данные повреждены. Если вы снова увидите это сообщение, попробуйте сбросить настройки устройства и удалить все пользовательские данные."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Повторить попытку"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Сбросить настройки"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Стереть все пользовательские данные?\n\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Отмена"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-si/strings.xml b/tools/recovery_l10n/res/values-si/strings.xml
index 456cdc5..67aca72 100644
--- a/tools/recovery_l10n/res/values-si/strings.xml
+++ b/tools/recovery_l10n/res/values-si/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"විධානයක් නොමැත"</string>
     <string name="recovery_error" msgid="5748178989622716736">"දෝෂය!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"ආරක්ෂක යාවත්කාලීනය ස්ථාපනය කරමින්"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android පද්ධතිය පූරණය කළ නොහැකිය. ඔබේ දත්ත දූෂිත විය හැකිය. ඔබට මෙම පණිවිඩය දිගටම ලැබෙන්නේ නම්, කර්මාන්ත ශාලා දත්ත යළි සැකසීමක් සිදු කර මෙම උපාංගයේ ගබඩා කළ සියලු පරිශීලක දත්ත මකා දැමීමට ඔබට අවශ්‍ය විය හැකිය."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"නැවත උත්සාහ කරන්න"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"කර්මාන්ත ශාලා දත්ත යළි සැකසීම"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"සියලු පරිශීලක දත්ත මකා දමන්නද?\n\n මෙය පසුගමනය කළ නොහැකිය!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"අවලංගු කරන්න"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sk/strings.xml b/tools/recovery_l10n/res/values-sk/strings.xml
index b15f380..8a2d2e0 100644
--- a/tools/recovery_l10n/res/values-sk/strings.xml
+++ b/tools/recovery_l10n/res/values-sk/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Žiadny príkaz"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Chyba!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Inštaluje sa bezpečnostná aktualizácia"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Systém Android sa nedá načítať. Vaše údaje môžu byť poškodené. Ak chcete získať túto správu a budete pokračovať, zrejme budete musieť obnoviť výrobné nastavenia a vymazať tak všetky údaje používateľa uložené v tomto zariadení."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Skúsiť znova"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Obnovenie výrobných nastavení"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Chcete vymazať všetky údaje používateľa?\n\n TÁTO AKCIA SA NEDÁ VRÁTIŤ SPÄŤ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Zrušiť"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sl/strings.xml b/tools/recovery_l10n/res/values-sl/strings.xml
index d608b75..653c427 100644
--- a/tools/recovery_l10n/res/values-sl/strings.xml
+++ b/tools/recovery_l10n/res/values-sl/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Ni ukaza"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Napaka"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Nameščanje varnostne posodobitve"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sistema Android ni mogoče naložiti. Podatki so morda poškodovani. Če se bo to sporočilo še naprej prikazovalo, boste morda morali izvesti ponastavitev na tovarniške nastavitve in izbrisati vse uporabniške podatke, ki so shranjeni v tej napravi."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Poskusi znova"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ponastavitev na tovarniške nastavitve"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite izbrisati vse uporabniške podatke?\n\n TEGA NI MOGOČE RAZVELJAVITI!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Prekliči"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sq/strings.xml b/tools/recovery_l10n/res/values-sq/strings.xml
index 1156931..5c824e6 100644
--- a/tools/recovery_l10n/res/values-sq/strings.xml
+++ b/tools/recovery_l10n/res/values-sq/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Nuk ka komanda"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Gabim!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Po instalon përditësimin e sigurisë"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sistemi Android nuk mund të ngarkohet. Të dhënat e tua mund të jenë të dëmtuara. Nëse vazhdon të marrësh këtë mesazh, mund të jetë e nevojshme të kryesh një rivendosje të të dhënave të fabrikës dhe të spastrosh të gjitha të dhënat e përdoruesit të ruajtura në këtë pajisje."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Provo përsëri"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rivendosja e të dhënave të fabrikës"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Të pastrohen të gjitha të dhënat e përdoruesit?\n\n KJO NUK MUND TË ZHBËHET!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anulo"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sr/strings.xml b/tools/recovery_l10n/res/values-sr/strings.xml
index a593d8f..1583bea 100644
--- a/tools/recovery_l10n/res/values-sr/strings.xml
+++ b/tools/recovery_l10n/res/values-sr/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Нема команде"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Инсталира се безбедносно ажурирање"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Учитавање Android система није успело. Подаци су можда оштећени. Ако наставите да добијате ову поруку, можда ћете морати да ресетујете уређај на фабричка подешавања и обришете све податке корисника које чувате на њему."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Пробај поново"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ресетовање на фабричка подешавања"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Желите ли да избришете све податке корисника?\n\n ОВО НЕ МОЖЕ ДА СЕ ОПОЗОВЕ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Откажи"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sv/strings.xml b/tools/recovery_l10n/res/values-sv/strings.xml
index b33ce25..cf43b25 100644
--- a/tools/recovery_l10n/res/values-sv/strings.xml
+++ b/tools/recovery_l10n/res/values-sv/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Inget kommando"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Fel!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Säkerhetsuppdatering installeras"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Det gick inte att läsa in Android-systemet. Data kan ha skadats. Om det här meddelandet visas igen kan du behöva återställa standardinställningarna så att all användardata som sparats på enheten raderas."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Försök igen"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Återställ standardinställningarna"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vill du rensa bort all användardata?\n\n DET GÅR INTE ATT ÅNGRA DENNA ÅTGÄRD."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Avbryt"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-sw/strings.xml b/tools/recovery_l10n/res/values-sw/strings.xml
index 1567658..6fa7282 100644
--- a/tools/recovery_l10n/res/values-sw/strings.xml
+++ b/tools/recovery_l10n/res/values-sw/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Hakuna amri"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Hitilafu fulani imetokea!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Inasakinisha sasisho la usalama"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Imeshindwa kupakia mfumo wa Android. Huenda data yako imeharibika. Kama utandelea kupata ujumbe huu, huenda ukahitaji kurejesha data iliyotoka nayo kiwandani na ufute data yote ya mtumiaji iliyohifadhiwa kwenye kifaa hiki."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Jaribu tena"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Kurejesha data iliyotoka nayo kiwandani"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ungependa kufuta data yote ya mtumiaji?\n\n KITENDO HIKI HAKIWEZI KUTENDULIWA!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ghairi"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ta/strings.xml b/tools/recovery_l10n/res/values-ta/strings.xml
index d49186d..bc370f7 100644
--- a/tools/recovery_l10n/res/values-ta/strings.xml
+++ b/tools/recovery_l10n/res/values-ta/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"கட்டளை இல்லை"</string>
     <string name="recovery_error" msgid="5748178989622716736">"பிழை!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"பாதுகாப்புப் புதுப்பிப்பை நிறுவுகிறது"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android சிஸ்டத்தைக் காண்பிக்க இயலவில்லை. உங்களின் தரவு சிதைந்திருக்கலாம். இந்த மெசேஜ் உங்களுக்குத் தொடர்ந்து வந்தால், தரவின் ஆரம்பநிலைக்கு மீட்டமைத்தல் மற்றும் இந்தச் சாதனத்தில் சேமிக்கப்பட்டுள்ள அனைத்துப் பயனர் தரவையும் அழித்தல் ஆகியவற்றைச் செய்ய வேண்டியிருக்கலாம்."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"மீண்டும் முயல்க"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"தரவின் ஆரம்பநிலை மீட்டமைப்பு"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"பயனரின் அனைத்துத் தரவையும் நீக்கவா?\n\n இதைச் செயல்தவிர்க்க இயலாது!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"இல்லை"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-te/strings.xml b/tools/recovery_l10n/res/values-te/strings.xml
index e35c82b..4d52114 100644
--- a/tools/recovery_l10n/res/values-te/strings.xml
+++ b/tools/recovery_l10n/res/values-te/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ఆదేశం లేదు"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ఎర్రర్ సంభవించింది!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"భద్రతా నవీకరణను ఇన్‌స్టాల్ చేస్తోంది"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android సిస్టమ్‌ని లోడ్ చేయడం సాధ్యం కాదు. మీ డేటా పాడై ఉండవచ్చు. మీకు ఈ సందేశం వస్తూనే ఉంటే, మీరు ఫ్యాక్టరీ డేటా రీసెట్ చేసి, పరికరంలో నిల్వ అయిన వినియోగదారు డేటా మొత్తాన్ని తొలగించాల్సి రావచ్చు."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"మళ్లీ ప్రయత్నించు"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ఫ్యాక్టరీ డేటా రీసెట్"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"వినియోగదారు డేటా మొత్తాన్ని తొలగించాలా?\n\n ఈ చర్యను రద్దు చేయలేరు!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"రద్దు చేయి"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-th/strings.xml b/tools/recovery_l10n/res/values-th/strings.xml
index 155affe..83d445d 100644
--- a/tools/recovery_l10n/res/values-th/strings.xml
+++ b/tools/recovery_l10n/res/values-th/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"ไม่มีคำสั่ง"</string>
     <string name="recovery_error" msgid="5748178989622716736">"ข้อผิดพลาด!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"กำลังติดตั้งการอัปเดตความปลอดภัย"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"โหลดระบบ Android ไม่ได้ ข้อมูลของคุณอาจเสียหาย หากคุณยังคงได้รับข้อความนี้อยู่ คุณอาจต้องรีเซ็ตข้อมูลเป็นค่าเริ่มต้นและลบข้อมูลผู้ใช้ทั้งหมดที่เก็บอยู่ในอุปกรณ์นี้"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"ลองอีกครั้ง"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"รีเซ็ตข้อมูลเป็นค่าเริ่มต้น"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ต้องการล้างข้อมูลผู้ใช้ทั้งหมดใช่ไหม\n\n การกระทำนี้จะยกเลิกไม่ได้"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ยกเลิก"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-tl/strings.xml b/tools/recovery_l10n/res/values-tl/strings.xml
index 555b42b..6621473 100644
--- a/tools/recovery_l10n/res/values-tl/strings.xml
+++ b/tools/recovery_l10n/res/values-tl/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Walang command"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Error!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Nag-i-install ng update sa seguridad"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Hindi ma-load ang Android system. Maaaring sira ang iyong data. Kung patuloy mong matatanggap ang mensaheng ito, maaaring kailanganin mong magsagawa ng pag-reset ng factory data at burahin ang lahat ng data ng user na naka-store sa device na ito."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Subukang muli"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Pag-reset ng factory data"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"I-wipe ang lahat ng data ng user?\n\n HINDI ITO MAA-UNDO!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Kanselahin"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-tr/strings.xml b/tools/recovery_l10n/res/values-tr/strings.xml
index 5387cb2..e4eca52 100644
--- a/tools/recovery_l10n/res/values-tr/strings.xml
+++ b/tools/recovery_l10n/res/values-tr/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Komut yok"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Hata!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Güvenlik güncellemesi yükleniyor"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android sisteminiz yüklenemedi. Verileriniz bozulmuş olabilir. Bu mesajı almaya devam ederseniz fabrika verilerine sıfırlama işlemi yapmanız ve bu cihazda depolanan tüm kullanıcı verilerini silmeniz gerekebilir."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Tekrar dene"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Fabrika verilerine sıfırla"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Tüm kullanıcı verileri silinsin mi?\n\n BU İŞLEM GERİ ALINAMAZ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"İptal"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-uk/strings.xml b/tools/recovery_l10n/res/values-uk/strings.xml
index 0c2fa16..7bd6fec 100644
--- a/tools/recovery_l10n/res/values-uk/strings.xml
+++ b/tools/recovery_l10n/res/values-uk/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Немає команди"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Помилка!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Установлюється оновлення системи безпеки"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не вдається завантажити систему Android. Можливо, ваші дані пошкоджено. Якщо ви далі отримуватимете це повідомлення, можливо, доведеться відновити заводські налаштування й видалити всі дані користувача з цього пристрою."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Повторити"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Відновити заводські налаштування"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Видалити всі дані користувача?\n\n ЦЮ ДІЮ НЕ МОЖНА ВІДМІНИТИ."</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Скасувати"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-ur/strings.xml b/tools/recovery_l10n/res/values-ur/strings.xml
index 12e32fb..da03f19 100644
--- a/tools/recovery_l10n/res/values-ur/strings.xml
+++ b/tools/recovery_l10n/res/values-ur/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"کوئی کمانڈ نہیں ہے"</string>
     <string name="recovery_error" msgid="5748178989622716736">"خرابی!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"سیکیورٹی اپ ڈیٹ انسٹال ہو رہی ہے"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"‏Android سسٹم لوڈ نہیں کیا جا سکتا۔ آپ کا ڈیٹا خراب ہو سکتا ہے۔ اگر آپ کو مستقل یہ پیغام موصول ہوتا ہے تو آپ کو فیکٹری ڈیٹا کی دوبارہ ترتیب انجام دینے اور اس آلہ پر اسٹور کردہ سبھی صارف ڈیٹا کو مٹانے کی ضرورت پڑ سکتی ہے۔"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"دوبارہ کوشش کریں"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"فیکٹری ڈیٹا کی دوبارہ ترتیب"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"سبھی صارف ڈیٹا صاف کریں؟\n\n اسے کالعدم نہیں کیا جا سکتا!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"منسوخ کریں"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-uz/strings.xml b/tools/recovery_l10n/res/values-uz/strings.xml
index 2c309d6..9bde4c6 100644
--- a/tools/recovery_l10n/res/values-uz/strings.xml
+++ b/tools/recovery_l10n/res/values-uz/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Buyruq yo‘q"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Xato!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Xavfsizlik yangilanishi o‘rnatilmoqda"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android tizimi yuklanmadi. Maʼlumotlaringiz buzuq shekilli. Yana shu xabarni olsangiz, zavod sozlamalarini tiklashingiz va bu qurilmadagi barcha maʼlumotlarni tozalab tashlashingiz lozim."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Qayta urinish"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Zavod sozlamalarini tiklash"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Barcha maʼlumotlar tozalab tashlansinmi?\n\n ULARNI TIKLASH IMKONSIZ!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Bekor qilish"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-vi/strings.xml b/tools/recovery_l10n/res/values-vi/strings.xml
index c77d0c8..3753394 100644
--- a/tools/recovery_l10n/res/values-vi/strings.xml
+++ b/tools/recovery_l10n/res/values-vi/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Không có lệnh nào"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Lỗi!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Đang cài đặt bản cập nhật bảo mật"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Không thể tải hệ thống Android. Dữ liệu của bạn có thể bị hỏng. Nếu tiếp tục thấy thông báo này, bạn có thể cần phải thiết lập lại dữ liệu ban đầu và xóa tất cả dữ liệu người dùng lưu trữ trên thiết bị này."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Thử lại"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Thiết lập lại dữ liệu ban đầu"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Xóa sạch tất cả dữ liệu người dùng?\n\n KHÔNG THỂ HOÀN TÁC THAO TÁC NÀY!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Hủy"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-zh-rCN/strings.xml b/tools/recovery_l10n/res/values-zh-rCN/strings.xml
index e061497..ab1fdbb 100644
--- a/tools/recovery_l10n/res/values-zh-rCN/strings.xml
+++ b/tools/recovery_l10n/res/values-zh-rCN/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"无命令"</string>
     <string name="recovery_error" msgid="5748178989622716736">"出错了!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"正在安装安全更新"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"无法加载 Android 系统。您的数据可能已损坏。如果系统仍然显示这条消息,您可能需要恢复出厂设置,并清空存储在此设备上的所有用户数据。"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"重试"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"恢复出厂设置"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"是否清除所有用户数据?\n\n此操作无法撤消!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-zh-rHK/strings.xml b/tools/recovery_l10n/res/values-zh-rHK/strings.xml
index ec3315d..55ce31e 100644
--- a/tools/recovery_l10n/res/values-zh-rHK/strings.xml
+++ b/tools/recovery_l10n/res/values-zh-rHK/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"沒有指令"</string>
     <string name="recovery_error" msgid="5748178989622716736">"錯誤!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"正在安裝安全性更新"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"無法載入 Android 系統。您的資料可能已損壞。如您繼續收到此訊息,則可能需要將裝置回復原廠設定,並清除儲存在裝置上的所有使用者資料。"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"再試一次"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"回復原廠設定"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"要清除所有使用者資料嗎?\n\n這項操作無法復原!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-zh-rTW/strings.xml b/tools/recovery_l10n/res/values-zh-rTW/strings.xml
index 78eae24..0a777a6 100644
--- a/tools/recovery_l10n/res/values-zh-rTW/strings.xml
+++ b/tools/recovery_l10n/res/values-zh-rTW/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"沒有指令"</string>
     <string name="recovery_error" msgid="5748178989622716736">"錯誤!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"正在安裝安全性更新"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"無法載入 Android 系統。你的資料可能已經損毀。如果系統持續顯示這則訊息,你可能必須恢復原廠設定,並清除裝置上儲存的所有使用者資料。"</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"再試一次"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"恢復原廠設定"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"要抹除所有使用者資料嗎?\n\n請注意,一旦抹除就無法復原!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values-zu/strings.xml b/tools/recovery_l10n/res/values-zu/strings.xml
index 6b815e1..4667dac 100644
--- a/tools/recovery_l10n/res/values-zu/strings.xml
+++ b/tools/recovery_l10n/res/values-zu/strings.xml
@@ -6,4 +6,9 @@
     <string name="recovery_no_command" msgid="4465476568623024327">"Awukho umyalo"</string>
     <string name="recovery_error" msgid="5748178989622716736">"Iphutha!"</string>
     <string name="recovery_installing_security" msgid="9184031299717114342">"Ifaka isibuyekezo sokuphepha"</string>
+    <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ayikwazi ukulayisha isistimu ye-Android. Idatha yakho kungenzeka yonakele. Uma uqhubeka ukuthola lo mlayezo, kungenzeka kumele wenze ukusethwa kabusha kwasekuqaleni kwedatha uphinde usule yonke idatha yomsebenzisi egcinwe kule divayisi."</string>
+    <string name="recovery_try_again" msgid="7168248750158873496">"Zama futhi"</string>
+    <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ukuhlela kabusha idatha yasembonini"</string>
+    <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Sula yonke idatha yomsebenzisi?\n\n LOKHU AKUKWAZI UKUHLEHLISWA!"</string>
+    <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Khansela"</string>
 </resources>
diff --git a/tools/recovery_l10n/res/values/strings.xml b/tools/recovery_l10n/res/values/strings.xml
index d56d073..a557ba8 100644
--- a/tools/recovery_l10n/res/values/strings.xml
+++ b/tools/recovery_l10n/res/values/strings.xml
@@ -36,4 +36,36 @@
        system is installing a security update. [CHAR LIMIT=60] -->
   <string name="recovery_installing_security">Installing security update</string>
 
+  <!-- Displayed on the screen beneath the recovery titles when the
+       device enters the recovery mode and prompts a data wipe. [CHAR
+       LIMIT=400] -->
+  <string name="recovery_wipe_data_menu_header">Cannot 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.</string>
+
+  <!-- Displayed on the screen as the first element of the menu to
+       prompt the wipe data, beneath the menu header. The menu shows
+       up when the device enters the recovery mode and prompts a data
+       wipe. [CHAR LIMIT=60] -->
+  <string name="recovery_try_again">Try again</string>
+
+  <!-- Displayed on the screen as the second element of the menu to
+       prompt the wipe data, beneath the menu header. The menu shows
+       up when the device enters the recovery mode and prompts a data
+       wipe. [CHAR LIMIT=60] -->
+  <string name="recovery_factory_data_reset">Factory data reset</string>
+
+  <!-- Displayed on the screen beneath the recovery titles when users
+       select "Factory data reset" in the previous menu. [CHAR
+       LIMIT=150] -->
+  <string name="recovery_wipe_data_confirmation">Wipe all user data?\n\n
+    THIS CAN NOT BE UNDONE!</string>
+
+  <!-- Displayed on the screen as the first element of the wipe data
+       confirmation menu. The menu shows up when users select
+       "Factory data reset" when prompted to wipe data. [CHAR
+       LIMIT=60] -->
+  <string name="recovery_cancel_wipe_data">Cancel</string>
+
 </resources>
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..4924fec 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,46 @@
 
   // --- 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;
+  // Displays the localized wipe data menu with pre-generated graphs. If there's an issue
+  // with the graphs, falls back to use the backup string headers and items instead. The initial
+  // selection is the 0th item in the menu, which is expected to reboot the device without a wipe.
+  virtual size_t ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers,
+                                        const std::vector<std::string>& backup_items,
+                                        const std::function<int(int, bool)>& key_handler) = 0;
+
+  // Displays the localized wipe data confirmation menu with pre-generated images. Falls back to
+  // the text strings upon failures. The initial selection is the 0th item, which returns to the
+  // upper level menu.
+  virtual size_t ShowPromptWipeDataConfirmationMenu(
+      const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items,
+      const std::function<int(int, bool)>& key_handler) = 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 +208,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 +252,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..f1f4f69 100644
--- a/uncrypt/uncrypt.cpp
+++ b/uncrypt/uncrypt.cpp
@@ -89,7 +89,6 @@
 #include <errno.h>
 #include <fcntl.h>
 #include <inttypes.h>
-#include <libgen.h>
 #include <linux/fs.h>
 #include <stdarg.h>
 #include <stdio.h>
@@ -103,6 +102,7 @@
 
 #include <algorithm>
 #include <memory>
+#include <string>
 #include <vector>
 
 #include <android-base/file.h>
@@ -115,9 +115,13 @@
 #include <cutils/android_reboot.h>
 #include <cutils/sockets.h>
 #include <fs_mgr.h>
+#include <fstab/fstab.h>
 
 #include "otautil/error_code.h"
 
+using android::fs_mgr::Fstab;
+using android::fs_mgr::ReadDefaultFstab;
+
 static constexpr int WINDOW_SIZE = 5;
 static constexpr int FIBMAP_RETRY_LIMIT = 3;
 
@@ -136,7 +140,7 @@
 static const std::string UNCRYPT_STATUS = "/cache/recovery/uncrypt_status";
 static const std::string UNCRYPT_SOCKET = "uncrypt";
 
-static struct fstab* fstab = nullptr;
+static Fstab fstab;
 
 static int write_at_offset(unsigned char* buffer, size_t size, int wfd, off64_t offset) {
     if (TEMP_FAILURE_RETRY(lseek64(wfd, offset, SEEK_SET)) == -1) {
@@ -162,47 +166,34 @@
     }
 }
 
-static struct fstab* read_fstab() {
-    fstab = fs_mgr_read_fstab_default();
-    if (!fstab) {
-        LOG(ERROR) << "failed to read default fstab";
-        return NULL;
+// Looks for a volume whose mount point is the prefix of path and returns its block device or an
+// empty string. Sets encryption flags accordingly.
+static std::string FindBlockDevice(const std::string& path, bool* encryptable, bool* encrypted,
+                                   bool* f2fs_fs) {
+  // Ensure f2fs_fs is set to false first.
+  *f2fs_fs = false;
+
+  for (const auto& entry : fstab) {
+    if (entry.mount_point.empty()) {
+      continue;
     }
-
-    return fstab;
-}
-
-static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted, bool *f2fs_fs) {
-    // Look for a volume whose mount point is the prefix of path and
-    // return its block device.  Set encrypted if it's currently
-    // encrypted.
-
-    // ensure f2fs_fs is set to 0 first.
-    if (f2fs_fs)
-        *f2fs_fs = false;
-    for (int i = 0; i < fstab->num_entries; ++i) {
-        struct fstab_rec* v = &fstab->recs[i];
-        if (!v->mount_point) {
-            continue;
+    if (android::base::StartsWith(path, entry.mount_point + "/")) {
+      *encrypted = false;
+      *encryptable = false;
+      if (entry.is_encryptable() || entry.fs_mgr_flags.file_encryption) {
+        *encryptable = true;
+        if (android::base::GetProperty("ro.crypto.state", "") == "encrypted") {
+          *encrypted = true;
         }
-        int len = strlen(v->mount_point);
-        if (strncmp(path, v->mount_point, len) == 0 &&
-            (path[len] == '/' || path[len] == 0)) {
-            *encrypted = false;
-            *encryptable = false;
-            if (fs_mgr_is_encryptable(v) || fs_mgr_is_file_encrypted(v)) {
-                *encryptable = true;
-                if (android::base::GetProperty("ro.crypto.state", "") == "encrypted") {
-                    *encrypted = true;
-                }
-            }
-            if (f2fs_fs && strcmp(v->fs_type, "f2fs") == 0)
-                *f2fs_fs = true;
-            return v->blk_device;
-        }
+      }
+      if (entry.fs_type == "f2fs") {
+        *f2fs_fs = true;
+      }
+      return entry.blk_device;
     }
+  }
 
-    return NULL;
+  return "";
 }
 
 static bool write_status_to_socket(int status, int socket) {
@@ -215,113 +206,127 @@
     return android::base::WriteFully(socket, &status_out, sizeof(int));
 }
 
-// Parse uncrypt_file to find the update package name.
-static bool find_uncrypt_package(const std::string& uncrypt_path_file, std::string* package_name) {
-    CHECK(package_name != nullptr);
-    std::string uncrypt_path;
-    if (!android::base::ReadFileToString(uncrypt_path_file, &uncrypt_path)) {
-        PLOG(ERROR) << "failed to open \"" << uncrypt_path_file << "\"";
-        return false;
-    }
+// Parses the given path file to find the update package name.
+static bool FindUncryptPackage(const std::string& uncrypt_path_file, std::string* package_name) {
+  CHECK(package_name != nullptr);
+  std::string uncrypt_path;
+  if (!android::base::ReadFileToString(uncrypt_path_file, &uncrypt_path)) {
+    PLOG(ERROR) << "failed to open \"" << uncrypt_path_file << "\"";
+    return false;
+  }
 
-    // Remove the trailing '\n' if present.
-    *package_name = android::base::Trim(uncrypt_path);
-    return true;
+  // Remove the trailing '\n' if present.
+  *package_name = android::base::Trim(uncrypt_path);
+  return true;
 }
 
-static int retry_fibmap(const int fd, const char* name, int* block, const int head_block) {
-    CHECK(block != nullptr);
-    for (size_t i = 0; i < FIBMAP_RETRY_LIMIT; i++) {
-        if (fsync(fd) == -1) {
-            PLOG(ERROR) << "failed to fsync \"" << name << "\"";
-            return kUncryptFileSyncError;
-        }
-        if (ioctl(fd, FIBMAP, block) != 0) {
-            PLOG(ERROR) << "failed to find block " << head_block;
-            return kUncryptIoctlError;
-        }
-        if (*block != 0) {
-            return kUncryptNoError;
-        }
-        sleep(1);
+static int RetryFibmap(int fd, const std::string& name, int* block, const int head_block) {
+  CHECK(block != nullptr);
+  for (size_t i = 0; i < FIBMAP_RETRY_LIMIT; i++) {
+    if (fsync(fd) == -1) {
+      PLOG(ERROR) << "failed to fsync \"" << name << "\"";
+      return kUncryptFileSyncError;
     }
-    LOG(ERROR) << "fibmap of " << head_block << "always returns 0";
-    return kUncryptIoctlError;
+    if (ioctl(fd, FIBMAP, block) != 0) {
+      PLOG(ERROR) << "failed to find block " << head_block;
+      return kUncryptIoctlError;
+    }
+    if (*block != 0) {
+      return kUncryptNoError;
+    }
+    sleep(1);
+  }
+  LOG(ERROR) << "fibmap of " << head_block << " always returns 0";
+  return kUncryptIoctlError;
 }
 
-static int produce_block_map(const char* path, const char* map_file, const char* blk_dev,
-                             bool encrypted, bool f2fs_fs, int socket) {
-    std::string err;
-    if (!android::base::RemoveFileIfExists(map_file, &err)) {
-        LOG(ERROR) << "failed to remove the existing map file " << map_file << ": " << err;
-        return kUncryptFileRemoveError;
+static int ProductBlockMap(const std::string& path, const std::string& map_file,
+                           const std::string& blk_dev, bool encrypted, bool f2fs_fs, int socket) {
+  std::string err;
+  if (!android::base::RemoveFileIfExists(map_file, &err)) {
+    LOG(ERROR) << "failed to remove the existing map file " << map_file << ": " << err;
+    return kUncryptFileRemoveError;
+  }
+  std::string tmp_map_file = map_file + ".tmp";
+  android::base::unique_fd mapfd(open(tmp_map_file.c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR));
+  if (mapfd == -1) {
+    PLOG(ERROR) << "failed to open " << tmp_map_file;
+    return kUncryptFileOpenError;
+  }
+
+  // Make sure we can write to the socket.
+  if (!write_status_to_socket(0, socket)) {
+    LOG(ERROR) << "failed to write to socket " << socket;
+    return kUncryptSocketWriteError;
+  }
+
+  struct stat sb;
+  if (stat(path.c_str(), &sb) != 0) {
+    PLOG(ERROR) << "failed to stat " << path;
+    return kUncryptFileStatError;
+  }
+
+  LOG(INFO) << " block size: " << sb.st_blksize << " bytes";
+
+  int blocks = ((sb.st_size - 1) / sb.st_blksize) + 1;
+  LOG(INFO) << "  file size: " << sb.st_size << " bytes, " << blocks << " blocks";
+
+  std::vector<int> ranges;
+
+  std::string s = android::base::StringPrintf("%s\n%" PRId64 " %" PRId64 "\n", blk_dev.c_str(),
+                                              static_cast<int64_t>(sb.st_size),
+                                              static_cast<int64_t>(sb.st_blksize));
+  if (!android::base::WriteStringToFd(s, mapfd)) {
+    PLOG(ERROR) << "failed to write " << tmp_map_file;
+    return kUncryptWriteError;
+  }
+
+  std::vector<std::vector<unsigned char>> buffers;
+  if (encrypted) {
+    buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize));
+  }
+  int head_block = 0;
+  int head = 0, tail = 0;
+
+  android::base::unique_fd fd(open(path.c_str(), O_RDWR));
+  if (fd == -1) {
+    PLOG(ERROR) << "failed to open " << path << " for reading";
+    return kUncryptFileOpenError;
+  }
+
+  android::base::unique_fd wfd;
+  if (encrypted) {
+    wfd.reset(open(blk_dev.c_str(), O_WRONLY));
+    if (wfd == -1) {
+      PLOG(ERROR) << "failed to open " << blk_dev << " for writing";
+      return kUncryptBlockOpenError;
     }
-    std::string tmp_map_file = std::string(map_file) + ".tmp";
-    android::base::unique_fd mapfd(open(tmp_map_file.c_str(),
-                                        O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR));
-    if (mapfd == -1) {
-        PLOG(ERROR) << "failed to open " << tmp_map_file;
-        return kUncryptFileOpenError;
-    }
+  }
 
-    // Make sure we can write to the socket.
-    if (!write_status_to_socket(0, socket)) {
-        LOG(ERROR) << "failed to write to socket " << socket;
-        return kUncryptSocketWriteError;
-    }
-
-    struct stat sb;
-    if (stat(path, &sb) != 0) {
-        LOG(ERROR) << "failed to stat " << path;
-        return kUncryptFileStatError;
-    }
-
-    LOG(INFO) << " block size: " << sb.st_blksize << " bytes";
-
-    int blocks = ((sb.st_size-1) / sb.st_blksize) + 1;
-    LOG(INFO) << "  file size: " << sb.st_size << " bytes, " << blocks << " blocks";
-
-    std::vector<int> ranges;
-
-    std::string s = android::base::StringPrintf("%s\n%" PRId64 " %" PRId64 "\n",
-                       blk_dev, static_cast<int64_t>(sb.st_size),
-                       static_cast<int64_t>(sb.st_blksize));
-    if (!android::base::WriteStringToFd(s, mapfd)) {
-        PLOG(ERROR) << "failed to write " << tmp_map_file;
-        return kUncryptWriteError;
-    }
-
-    std::vector<std::vector<unsigned char>> buffers;
-    if (encrypted) {
-        buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize));
-    }
-    int head_block = 0;
-    int head = 0, tail = 0;
-
-    android::base::unique_fd fd(open(path, O_RDONLY));
-    if (fd == -1) {
-        PLOG(ERROR) << "failed to open " << path << " for reading";
-        return kUncryptFileOpenError;
-    }
-
-    android::base::unique_fd wfd;
-    if (encrypted) {
-        wfd.reset(open(blk_dev, O_WRONLY));
-        if (wfd == -1) {
-            PLOG(ERROR) << "failed to open " << blk_dev << " for writing";
-            return kUncryptBlockOpenError;
-        }
-    }
-
-#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	_IOR(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;
@@ -344,7 +349,7 @@
 
             if (block == 0) {
                 LOG(ERROR) << "failed to find block " << head_block << ", retrying";
-                int error = retry_fibmap(fd, path, &block, head_block);
+                int error = RetryFibmap(fd, path, &block, head_block);
                 if (error != kUncryptNoError) {
                     return error;
                 }
@@ -389,7 +394,7 @@
 
         if (block == 0) {
             LOG(ERROR) << "failed to find block " << head_block << ", retrying";
-            int error = retry_fibmap(fd, path, &block, head_block);
+            int error = RetryFibmap(fd, path, &block, head_block);
             if (error != kUncryptNoError) {
                 return error;
             }
@@ -439,13 +444,12 @@
         }
     }
 
-    if (rename(tmp_map_file.c_str(), map_file) == -1) {
-        PLOG(ERROR) << "failed to rename " << tmp_map_file << " to " << map_file;
-        return kUncryptFileRenameError;
+    if (rename(tmp_map_file.c_str(), map_file.c_str()) == -1) {
+      PLOG(ERROR) << "failed to rename " << tmp_map_file << " to " << map_file;
+      return kUncryptFileRenameError;
     }
     // Sync dir to make rename() result written to disk.
-    std::string file_name = map_file;
-    std::string dir_name = dirname(&file_name[0]);
+    std::string dir_name = android::base::Dirname(map_file);
     android::base::unique_fd dfd(open(dir_name.c_str(), O_RDONLY | O_DIRECTORY));
     if (dfd == -1) {
         PLOG(ERROR) << "failed to open dir " << dir_name;
@@ -462,45 +466,42 @@
     return 0;
 }
 
-static int uncrypt(const char* input_path, const char* map_file, const int socket) {
-    LOG(INFO) << "update package is \"" << input_path << "\"";
+static int Uncrypt(const std::string& input_path, const std::string& map_file, int socket) {
+  LOG(INFO) << "update package is \"" << input_path << "\"";
 
-    // Turn the name of the file we're supposed to convert into an absolute path, so we can find
-    // what filesystem it's on.
-    char path[PATH_MAX+1];
-    if (realpath(input_path, path) == nullptr) {
-        PLOG(ERROR) << "failed to convert \"" << input_path << "\" to absolute path";
-        return kUncryptRealpathFindError;
-    }
+  // Turn the name of the file we're supposed to convert into an absolute path, so we can find what
+  // filesystem it's on.
+  std::string path;
+  if (!android::base::Realpath(input_path, &path)) {
+    PLOG(ERROR) << "Failed to convert \"" << input_path << "\" to absolute path";
+    return kUncryptRealpathFindError;
+  }
 
-    bool encryptable;
-    bool encrypted;
-    bool f2fs_fs;
-    const char* blk_dev = find_block_device(path, &encryptable, &encrypted, &f2fs_fs);
-    if (blk_dev == nullptr) {
-        LOG(ERROR) << "failed to find block device for " << path;
-        return kUncryptBlockDeviceFindError;
-    }
+  bool encryptable;
+  bool encrypted;
+  bool f2fs_fs;
+  const std::string blk_dev = FindBlockDevice(path, &encryptable, &encrypted, &f2fs_fs);
+  if (blk_dev.empty()) {
+    LOG(ERROR) << "Failed to find block device for " << path;
+    return kUncryptBlockDeviceFindError;
+  }
 
-    // If the filesystem it's on isn't encrypted, we only produce the
-    // block map, we don't rewrite the file contents (it would be
-    // pointless to do so).
-    LOG(INFO) << "encryptable: " << (encryptable ? "yes" : "no");
-    LOG(INFO) << "  encrypted: " << (encrypted ? "yes" : "no");
+  // If the filesystem it's on isn't encrypted, we only produce the block map, we don't rewrite the
+  // file contents (it would be pointless to do so).
+  LOG(INFO) << "encryptable: " << (encryptable ? "yes" : "no");
+  LOG(INFO) << "  encrypted: " << (encrypted ? "yes" : "no");
 
-    // Recovery supports installing packages from 3 paths: /cache,
-    // /data, and /sdcard.  (On a particular device, other locations
-    // may work, but those are three we actually expect.)
-    //
-    // On /data we want to convert the file to a block map so that we
-    // can read the package without mounting the partition.  On /cache
-    // and /sdcard we leave the file alone.
-    if (strncmp(path, "/data/", 6) == 0) {
-        LOG(INFO) << "writing block map " << map_file;
-        return produce_block_map(path, map_file, blk_dev, encrypted, f2fs_fs, socket);
-    }
+  // Recovery supports installing packages from 3 paths: /cache, /data, and /sdcard. (On a
+  // particular device, other locations may work, but those are three we actually expect.)
+  //
+  // On /data we want to convert the file to a block map so that we can read the package without
+  // mounting the partition. On /cache and /sdcard we leave the file alone.
+  if (android::base::StartsWith(path, "/data/")) {
+    LOG(INFO) << "writing block map " << map_file;
+    return ProductBlockMap(path, map_file, blk_dev, encrypted, f2fs_fs, socket);
+  }
 
-    return 0;
+  return 0;
 }
 
 static void log_uncrypt_error_code(UncryptErrorCode error_code) {
@@ -516,18 +517,18 @@
 
     std::string package;
     if (input_path == nullptr) {
-        if (!find_uncrypt_package(UNCRYPT_PATH_FILE, &package)) {
-            write_status_to_socket(-1, socket);
-            // Overwrite the error message.
-            log_uncrypt_error_code(kUncryptPackageMissingError);
-            return false;
-        }
-        input_path = package.c_str();
+      if (!FindUncryptPackage(UNCRYPT_PATH_FILE, &package)) {
+        write_status_to_socket(-1, socket);
+        // Overwrite the error message.
+        log_uncrypt_error_code(kUncryptPackageMissingError);
+        return false;
+      }
+      input_path = package.c_str();
     }
     CHECK(map_file != nullptr);
 
     auto start = std::chrono::system_clock::now();
-    int status = uncrypt(input_path, map_file, socket);
+    int status = Uncrypt(input_path, map_file, socket);
     std::chrono::duration<double> duration = std::chrono::system_clock::now() - start;
     int count = static_cast<int>(duration.count());
 
@@ -637,7 +638,8 @@
         return 2;
     }
 
-    if ((fstab = read_fstab()) == nullptr) {
+    if (!ReadDefaultFstab(&fstab)) {
+        LOG(ERROR) << "failed to read default fstab";
         log_uncrypt_error_code(kUncryptFstabReadError);
         return 1;
     }
diff --git a/update_verifier/Android.bp b/update_verifier/Android.bp
new file mode 100644
index 0000000..f656713
--- /dev/null
+++ b/update_verifier/Android.bp
@@ -0,0 +1,123 @@
+// 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",
+        "libvold_binder",
+    ],
+
+    shared_libs: [
+        "android.hardware.boot@1.0",
+        "libbase",
+        "libcutils",
+        "libbinder",
+        "libutils",
+    ],
+
+    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",
+        "libvold_binder",
+    ],
+
+    shared_libs: [
+        "android.hardware.boot@1.0",
+        "libbase",
+        "libcutils",
+        "libhardware",
+        "libhidlbase",
+        "liblog",
+        "libprotobuf-cpp-lite",
+        "libbinder",
+        "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..5e5eac7 100644
--- a/update_verifier/update_verifier.cpp
+++ b/update_verifier/update_verifier.cpp
@@ -15,27 +15,30 @@
  */
 
 /*
- * 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"
+#include <android/os/IVold.h>
 
 #include <dirent.h>
 #include <errno.h>
@@ -46,8 +49,6 @@
 
 #include <algorithm>
 #include <future>
-#include <string>
-#include <vector>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
@@ -56,15 +57,19 @@
 #include <android-base/strings.h>
 #include <android-base/unique_fd.h>
 #include <android/hardware/boot/1.0/IBootControl.h>
+#include <binder/BinderService.h>
+#include <binder/Status.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 +78,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 +108,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 +131,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 +142,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 +175,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 +196,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 +350,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,28 +365,44 @@
       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; });
-    if (!cr.success) {
-      LOG(ERROR) << "Error marking booted successfully: " << cr.errMsg;
-      return reboot_device();
+    bool supports_checkpoint = false;
+    auto sm = android::defaultServiceManager();
+    android::sp<android::IBinder> binder = sm->getService(android::String16("vold"));
+    if (binder) {
+      auto vold = android::interface_cast<android::os::IVold>(binder);
+      android::binder::Status status = vold->supportsCheckpoint(&supports_checkpoint);
+      if (!status.isOk()) {
+        LOG(ERROR) << "Failed to check if checkpoints supported. Continuing";
+      }
+    } else {
+      LOG(ERROR) << "Failed to obtain vold Binder. Continuing";
     }
-    LOG(INFO) << "Marked slot " << current_slot << " as booted successfully.";
+
+    if (!supports_checkpoint) {
+      CommandResult cr;
+      module->markBootSuccessful([&cr](CommandResult result) { cr = result; });
+      if (!cr.success) {
+        LOG(ERROR) << "Error marking booted successfully: " << cr.errMsg;
+        return reboot_device();
+      }
+      LOG(INFO) << "Marked slot " << current_slot << " as booted successfully.";
+    } else {
+      LOG(INFO) << "Deferred marking slot " << current_slot << " as booted successfully.";
+    }
   }
 
   LOG(INFO) << "Leaving update_verifier.";
diff --git a/updater/Android.bp b/updater/Android.bp
new file mode 100644
index 0000000..b80cdb3
--- /dev/null
+++ b/updater/Android.bp
@@ -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.
+
+cc_defaults {
+    name: "libupdater_defaults",
+
+    defaults: [
+        "recovery_defaults",
+    ],
+
+    static_libs: [
+        "libapplypatch",
+        "libbootloader_message",
+        "libbspatch",
+        "libedify",
+        "libotautil",
+        "libext4_utils",
+        "libdm",
+        "libfec",
+        "libfec_rs",
+        "libverity_tree",
+        "libfs_mgr",
+        "libgtest_prod",
+        "liblog",
+        "liblp",
+        "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",
+        "dynamic_partitions.cpp",
+        "install.cpp",
+    ],
+
+    include_dirs: [
+        "external/e2fsprogs/misc",
+    ],
+
+    export_include_dirs: [
+        "include",
+    ],
+}
diff --git a/updater/Android.mk b/updater/Android.mk
index 6f334ee..c7a6ba9 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -24,59 +24,34 @@
 
 updater_common_static_libraries := \
     libapplypatch \
+    libbootloader_message \
     libbspatch \
     libedify \
-    libziparchive \
     libotautil \
-    libbootloader_message \
-    libutils \
-    libmounts \
-    libotafault \
     libext4_utils \
+    libdm \
     libfec \
     libfec_rs \
+    libverity_tree \
     libfs_mgr \
+    libgtest_prod \
     liblog \
+    liblp \
     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 +62,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..07c3c7b 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,19 @@
 #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/dirutil.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"
 
@@ -68,13 +70,14 @@
 static constexpr size_t BLOCKSIZE = 4096;
 static constexpr mode_t STASH_DIRECTORY_MODE = 0700;
 static constexpr mode_t STASH_FILE_MODE = 0600;
+static constexpr mode_t MARKER_DIRECTORY_MODE = 0700;
 
 static CauseCode failure_type = kNoCause;
 static bool is_retry = false;
 static std::unordered_map<std::string, RangeSet> stash_map;
 
 static void DeleteLastCommandFile() {
-  std::string last_command_file = CacheLocation::location().last_command_file();
+  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 +85,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) {
@@ -108,7 +111,7 @@
     return false;
   }
 
-  if (!android::base::ParseInt(lines[0], last_command_index)) {
+  if (!android::base::ParseUint(lines[0], last_command_index)) {
     LOG(ERROR) << "Failed to parse integer in: " << lines[0];
     return false;
   }
@@ -116,10 +119,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,71 +161,44 @@
     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;
+bool SetUpdatedMarker(const std::string& marker) {
+  auto dirname = android::base::Dirname(marker);
+  auto res = mkdir(dirname.c_str(), MARKER_DIRECTORY_MODE);
+  if (res == -1 && errno != EEXIST) {
+    PLOG(ERROR) << "Failed to create directory for marker: " << dirname;
+    return false;
+  }
+
+  if (!android::base::WriteStringToFile("", marker)) {
+    PLOG(ERROR) << "Failed to write to marker file " << marker;
+    return false;
+  }
+  if (!FsyncDir(dirname)) {
+    return false;
+  }
+  LOG(INFO) << "Wrote updated marker to " << marker;
+  return true;
 }
 
-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 discard_blocks(int fd, off64_t offset, uint64_t size) {
-  // Don't discard blocks unless the update is a retry run.
-  if (!is_retry) {
+static bool discard_blocks(int fd, off64_t offset, uint64_t size, bool force = false) {
+  // Don't discard blocks unless the update is a retry run or force == true
+  if (!is_retry && !force) {
     return true;
   }
 
   uint64_t args[2] = { static_cast<uint64_t>(offset), size };
   if (ioctl(fd, BLKDISCARD, &args) == -1) {
+    // On devices that does not support BLKDISCARD, ignore the error.
+    if (errno == EOPNOTSUPP) {
+      return true;
+    }
     PLOG(ERROR) << "BLKDISCARD ioctl failed";
     return false;
   }
@@ -225,11 +215,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 +263,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 +474,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 +496,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 +507,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 +523,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 +638,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 +727,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 +740,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 +769,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,111 +802,92 @@
 }
 
 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);
+    res = mkdir_recursively(dirname, STASH_DIRECTORY_MODE, false, nullptr);
 
     if (res != 0) {
       ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s", dirname.c_str(),
@@ -927,7 +901,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 +933,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 +997,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 +1008,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 +1033,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 +1074,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 +1104,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 +1137,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 +1148,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 +1166,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 +1213,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 +1222,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 +1245,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 +1278,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 +1293,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 +1367,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 +1387,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 +1403,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;
@@ -1447,7 +1412,10 @@
 
       // We expect the output of the patcher to fill the tgt ranges exactly.
       if (!writer.Finished()) {
-        LOG(ERROR) << "range sink underrun?";
+        LOG(ERROR) << "Failed to fully write target blocks (range sink underrun): Missing "
+                   << writer.AvailableSpace() << " bytes";
+        failure_type = kPatchApplicationFailure;
+        return -1;
       }
     } else {
       LOG(INFO) << "skipping " << blocks << " blocks already patched to " << tgt.blocks() << " ["
@@ -1492,15 +1460,10 @@
   if (params.canwrite) {
     LOG(INFO) << " erasing " << tgt.blocks() << " blocks";
 
-    for (const auto& range : tgt) {
-      uint64_t blocks[2];
-      // offset in bytes
-      blocks[0] = range.first * static_cast<uint64_t>(BLOCKSIZE);
-      // length in bytes
-      blocks[1] = (range.second - range.first) * static_cast<uint64_t>(BLOCKSIZE);
-
-      if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) {
-        PLOG(ERROR) << "BLKDISCARD ioctl failed";
+    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, true /* force */)) {
         return -1;
       }
     }
@@ -1509,24 +1472,157 @@
   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(static_cast<int64_t>(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 bool Sha1DevicePath(const std::string& path, uint8_t digest[SHA_DIGEST_LENGTH]) {
+  auto device_name = android::base::Basename(path);
+  auto dm_target_name_path = "/sys/block/" + device_name + "/dm/name";
+
+  struct stat sb;
+  if (stat(dm_target_name_path.c_str(), &sb) == 0) {
+    // This is a device mapper target. Use partition name as part of the hash instead. Do not
+    // include extents as part of the hash, because the size of a partition may be shrunk after
+    // the patches are applied.
+    std::string dm_target_name;
+    if (!android::base::ReadFileToString(dm_target_name_path, &dm_target_name)) {
+      PLOG(ERROR) << "Cannot read " << dm_target_name_path;
+      return false;
+    }
+    SHA1(reinterpret_cast<const uint8_t*>(dm_target_name.data()), dm_target_name.size(), digest);
+    return true;
+  }
+
+  if (errno != ENOENT) {
+    // This is a device mapper target, but its name cannot be retrieved.
+    PLOG(ERROR) << "Cannot get dm target name for " << path;
+    return false;
+  }
+
+  // This doesn't appear to be a device mapper target, but if its name starts with dm-, something
+  // else might have gone wrong.
+  if (android::base::StartsWith(device_name, "dm-")) {
+    LOG(WARNING) << "Device " << path << " starts with dm- but is not mapped by device-mapper.";
+  }
+
+  // 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.
+  SHA1(reinterpret_cast<const uint8_t*>(path.data()), path.size(), digest);
+  return true;
+}
 
 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 +1641,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 +1695,44 @@
     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);
+  uint8_t digest[SHA_DIGEST_LENGTH];
+  if (!Sha1DevicePath(blockdev_filename->data, digest)) {
+    return StringValue("");
+  }
+  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 +1756,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 +1767,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 +1807,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);
     }
@@ -1796,14 +1915,23 @@
 
       const char* partition = strrchr(blockdev_filename->data.c_str(), '/');
       if (partition != nullptr && *(partition + 1) != 0) {
-        fprintf(cmd_pipe, "log bytes_written_%s: %zu\n", partition + 1, params.written * BLOCKSIZE);
-        fprintf(cmd_pipe, "log bytes_stashed_%s: %zu\n", partition + 1, params.stashed * BLOCKSIZE);
+        fprintf(cmd_pipe, "log bytes_written_%s: %" PRIu64 "\n", partition + 1,
+                static_cast<uint64_t>(params.written) * BLOCKSIZE);
+        fprintf(cmd_pipe, "log bytes_stashed_%s: %" PRIu64 "\n", partition + 1,
+                static_cast<uint64_t>(params.stashed) * BLOCKSIZE);
         fflush(cmd_pipe);
       }
       // Delete stash only after successfully completing the update, as it may contain blocks needed
       // to complete the update later.
       DeleteStash(params.stashbase);
       DeleteLastCommandFile();
+
+      // 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 (!SetUpdatedMarker(updated_marker)) {
+        LOG(WARNING) << "Failed to set updated marker; continuing";
+      }
     }
 
     pthread_mutex_destroy(&params.nti.mu);
@@ -1812,8 +1940,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 +2014,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 +2070,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 +2094,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 +2138,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 +2154,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 +2195,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 +2233,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/dynamic_partitions.cpp b/updater/dynamic_partitions.cpp
new file mode 100644
index 0000000..b50dd75
--- /dev/null
+++ b/updater/dynamic_partitions.cpp
@@ -0,0 +1,435 @@
+/*
+ * Copyright (C) 2019 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 "updater/dynamic_partitions.h"
+
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include <algorithm>
+#include <chrono>
+#include <iterator>
+#include <memory>
+#include <optional>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/strings.h>
+#include <fs_mgr.h>
+#include <fs_mgr_dm_linear.h>
+#include <libdm/dm.h>
+#include <liblp/builder.h>
+
+#include "edify/expr.h"
+#include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "private/utils.h"
+
+using android::base::ParseUint;
+using android::dm::DeviceMapper;
+using android::dm::DmDeviceState;
+using android::fs_mgr::CreateLogicalPartition;
+using android::fs_mgr::DestroyLogicalPartition;
+using android::fs_mgr::LpMetadata;
+using android::fs_mgr::MetadataBuilder;
+using android::fs_mgr::Partition;
+using android::fs_mgr::PartitionOpener;
+
+static constexpr std::chrono::milliseconds kMapTimeout{ 1000 };
+static constexpr char kMetadataUpdatedMarker[] = "/dynamic_partition_metadata.UPDATED";
+
+static std::string GetSuperDevice() {
+  return "/dev/block/by-name/" + fs_mgr_get_super_partition_name();
+}
+
+static std::vector<std::string> ReadStringArgs(const char* name, State* state,
+                                               const std::vector<std::unique_ptr<Expr>>& argv,
+                                               const std::vector<std::string>& arg_names) {
+  if (argv.size() != arg_names.size()) {
+    ErrorAbort(state, kArgsParsingFailure, "%s expects %zu arguments, got %zu", name,
+               arg_names.size(), argv.size());
+    return {};
+  }
+
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return {};
+  }
+
+  CHECK_EQ(args.size(), arg_names.size());
+
+  for (size_t i = 0; i < arg_names.size(); ++i) {
+    if (args[i]->type != Value::Type::STRING) {
+      ErrorAbort(state, kArgsParsingFailure, "%s argument to %s must be string",
+                 arg_names[i].c_str(), name);
+      return {};
+    }
+  }
+
+  std::vector<std::string> ret;
+  std::transform(args.begin(), args.end(), std::back_inserter(ret),
+                 [](const auto& arg) { return arg->data; });
+  return ret;
+}
+
+static bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) {
+  auto state = DeviceMapper::Instance().GetState(partition_name);
+  if (state == DmDeviceState::INVALID) {
+    return true;
+  }
+  if (state == DmDeviceState::ACTIVE) {
+    return DestroyLogicalPartition(partition_name, kMapTimeout);
+  }
+  LOG(ERROR) << "Unknown device mapper state: "
+             << static_cast<std::underlying_type_t<DmDeviceState>>(state);
+  return false;
+}
+
+static bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) {
+  auto state = DeviceMapper::Instance().GetState(partition_name);
+  if (state == DmDeviceState::INVALID) {
+    return CreateLogicalPartition(GetSuperDevice(), 0 /* metadata slot */, partition_name,
+                                  true /* force writable */, kMapTimeout, path);
+  }
+
+  if (state == DmDeviceState::ACTIVE) {
+    return DeviceMapper::Instance().GetDmDevicePathByName(partition_name, path);
+  }
+  LOG(ERROR) << "Unknown device mapper state: "
+             << static_cast<std::underlying_type_t<DmDeviceState>>(state);
+  return false;
+}
+
+Value* UnmapPartitionFn(const char* name, State* state,
+                        const std::vector<std::unique_ptr<Expr>>& argv) {
+  auto args = ReadStringArgs(name, state, argv, { "name" });
+  if (args.empty()) return StringValue("");
+
+  return UnmapPartitionOnDeviceMapper(args[0]) ? StringValue("t") : StringValue("");
+}
+
+Value* MapPartitionFn(const char* name, State* state,
+                      const std::vector<std::unique_ptr<Expr>>& argv) {
+  auto args = ReadStringArgs(name, state, argv, { "name" });
+  if (args.empty()) return StringValue("");
+
+  std::string path;
+  bool result = MapPartitionOnDeviceMapper(args[0], &path);
+  return result ? StringValue(path) : StringValue("");
+}
+
+namespace {  // Ops
+
+struct OpParameters {
+  std::vector<std::string> tokens;
+  MetadataBuilder* builder;
+
+  bool ExpectArgSize(size_t size) const {
+    CHECK(!tokens.empty());
+    auto actual = tokens.size() - 1;
+    if (actual != size) {
+      LOG(ERROR) << "Op " << op() << " expects " << size << " args, got " << actual;
+      return false;
+    }
+    return true;
+  }
+  const std::string& op() const {
+    CHECK(!tokens.empty());
+    return tokens[0];
+  }
+  const std::string& arg(size_t pos) const {
+    CHECK_LE(pos + 1, tokens.size());
+    return tokens[pos + 1];
+  }
+  std::optional<uint64_t> uint_arg(size_t pos, const std::string& name) const {
+    auto str = arg(pos);
+    uint64_t ret;
+    if (!ParseUint(str, &ret)) {
+      LOG(ERROR) << "Op " << op() << " expects uint64 for argument " << name << ", got " << str;
+      return std::nullopt;
+    }
+    return ret;
+  }
+};
+
+using OpFunction = std::function<bool(const OpParameters&)>;
+using OpMap = std::map<std::string, OpFunction>;
+
+bool PerformOpResize(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  auto size = params.uint_arg(1, "size");
+  if (!size.has_value()) return false;
+
+  auto partition = params.builder->FindPartition(partition_name);
+  if (partition == nullptr) {
+    LOG(ERROR) << "Failed to find partition " << partition_name
+               << " in dynamic partition metadata.";
+    return false;
+  }
+  if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+    LOG(ERROR) << "Cannot unmap " << partition_name << " before resizing.";
+    return false;
+  }
+  if (!params.builder->ResizePartition(partition, size.value())) {
+    LOG(ERROR) << "Failed to resize partition " << partition_name << " to size " << *size << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpRemove(const OpParameters& params) {
+  if (!params.ExpectArgSize(1)) return false;
+  const auto& partition_name = params.arg(0);
+
+  if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+    LOG(ERROR) << "Cannot unmap " << partition_name << " before removing.";
+    return false;
+  }
+  params.builder->RemovePartition(partition_name);
+  return true;
+}
+
+bool PerformOpAdd(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  const auto& group_name = params.arg(1);
+
+  if (params.builder->AddPartition(partition_name, group_name, LP_PARTITION_ATTR_READONLY) ==
+      nullptr) {
+    LOG(ERROR) << "Failed to add partition " << partition_name << " to group " << group_name << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpMove(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& partition_name = params.arg(0);
+  const auto& new_group = params.arg(1);
+
+  auto partition = params.builder->FindPartition(partition_name);
+  if (partition == nullptr) {
+    LOG(ERROR) << "Cannot move partition " << partition_name << " to group " << new_group
+               << " because it is not found.";
+    return false;
+  }
+
+  auto old_group = partition->group_name();
+  if (old_group != new_group) {
+    if (!params.builder->ChangePartitionGroup(partition, new_group)) {
+      LOG(ERROR) << "Cannot move partition " << partition_name << " from group " << old_group
+                 << " to group " << new_group << ".";
+      return false;
+    }
+  }
+  return true;
+}
+
+bool PerformOpAddGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& group_name = params.arg(0);
+  auto maximum_size = params.uint_arg(1, "maximum_size");
+  if (!maximum_size.has_value()) return false;
+
+  auto group = params.builder->FindGroup(group_name);
+  if (group != nullptr) {
+    LOG(ERROR) << "Cannot add group " << group_name << " because it already exists.";
+    return false;
+  }
+
+  if (maximum_size.value() == 0) {
+    LOG(WARNING) << "Adding group " << group_name << " with no size limits.";
+  }
+
+  if (!params.builder->AddGroup(group_name, maximum_size.value())) {
+    LOG(ERROR) << "Failed to add group " << group_name << " with maximum size "
+               << maximum_size.value() << ".";
+    return false;
+  }
+  return true;
+}
+
+bool PerformOpResizeGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(2)) return false;
+  const auto& group_name = params.arg(0);
+  auto new_size = params.uint_arg(1, "maximum_size");
+  if (!new_size.has_value()) return false;
+
+  auto group = params.builder->FindGroup(group_name);
+  if (group == nullptr) {
+    LOG(ERROR) << "Cannot resize group " << group_name << " because it is not found.";
+    return false;
+  }
+
+  auto old_size = group->maximum_size();
+  if (old_size != new_size.value()) {
+    if (!params.builder->ChangeGroupSize(group_name, new_size.value())) {
+      LOG(ERROR) << "Cannot resize group " << group_name << " from " << old_size << " to "
+                 << new_size.value() << ".";
+      return false;
+    }
+  }
+  return true;
+}
+
+std::vector<std::string> ListPartitionNamesInGroup(MetadataBuilder* builder,
+                                                   const std::string& group_name) {
+  auto partitions = builder->ListPartitionsInGroup(group_name);
+  std::vector<std::string> partition_names;
+  std::transform(partitions.begin(), partitions.end(), std::back_inserter(partition_names),
+                 [](Partition* partition) { return partition->name(); });
+  return partition_names;
+}
+
+bool PerformOpRemoveGroup(const OpParameters& params) {
+  if (!params.ExpectArgSize(1)) return false;
+  const auto& group_name = params.arg(0);
+
+  auto partition_names = ListPartitionNamesInGroup(params.builder, group_name);
+  if (!partition_names.empty()) {
+    LOG(ERROR) << "Cannot remove group " << group_name << " because it still contains partitions ["
+               << android::base::Join(partition_names, ", ") << "]";
+    return false;
+  }
+  params.builder->RemoveGroupAndPartitions(group_name);
+  return true;
+}
+
+bool PerformOpRemoveAllGroups(const OpParameters& params) {
+  if (!params.ExpectArgSize(0)) return false;
+
+  auto group_names = params.builder->ListGroups();
+  for (const auto& group_name : group_names) {
+    auto partition_names = ListPartitionNamesInGroup(params.builder, group_name);
+    for (const auto& partition_name : partition_names) {
+      if (!UnmapPartitionOnDeviceMapper(partition_name)) {
+        LOG(ERROR) << "Cannot unmap " << partition_name << " before removing group " << group_name
+                   << ".";
+        return false;
+      }
+    }
+    params.builder->RemoveGroupAndPartitions(group_name);
+  }
+  return true;
+}
+
+}  // namespace
+
+Value* UpdateDynamicPartitionsFn(const char* name, State* state,
+                                 const std::vector<std::unique_ptr<Expr>>& argv) {
+  if (argv.size() != 1) {
+    ErrorAbort(state, kArgsParsingFailure, "%s expects 1 arguments, got %zu", name, argv.size());
+    return StringValue("");
+  }
+  std::vector<std::unique_ptr<Value>> args;
+  if (!ReadValueArgs(state, argv, &args)) {
+    return nullptr;
+  }
+  const std::unique_ptr<Value>& op_list_value = args[0];
+  if (op_list_value->type != Value::Type::BLOB) {
+    ErrorAbort(state, kArgsParsingFailure, "op_list argument to %s must be blob", name);
+    return StringValue("");
+  }
+
+  std::string updated_marker = Paths::Get().stash_directory_base() + kMetadataUpdatedMarker;
+  if (state->is_retry) {
+    struct stat sb;
+    int result = stat(updated_marker.c_str(), &sb);
+    if (result == 0) {
+      LOG(INFO) << "Skipping already updated dynamic partition metadata based on marker";
+      return StringValue("t");
+    }
+  } else {
+    // Delete the obsolete marker if any.
+    std::string err;
+    if (!android::base::RemoveFileIfExists(updated_marker, &err)) {
+      LOG(ERROR) << "Failed to remove dynamic partition metadata updated marker " << updated_marker
+                 << ": " << err;
+      return StringValue("");
+    }
+  }
+
+  auto super_device = GetSuperDevice();
+  auto builder = MetadataBuilder::New(PartitionOpener(), super_device, 0);
+  if (builder == nullptr) {
+    LOG(ERROR) << "Failed to load dynamic partition metadata.";
+    return StringValue("");
+  }
+
+  static const OpMap op_map{
+    // clang-format off
+    {"resize",                PerformOpResize},
+    {"remove",                PerformOpRemove},
+    {"add",                   PerformOpAdd},
+    {"move",                  PerformOpMove},
+    {"add_group",             PerformOpAddGroup},
+    {"resize_group",          PerformOpResizeGroup},
+    {"remove_group",          PerformOpRemoveGroup},
+    {"remove_all_groups",     PerformOpRemoveAllGroups},
+    // clang-format on
+  };
+
+  std::vector<std::string> lines = android::base::Split(op_list_value->data, "\n");
+  for (const auto& line : lines) {
+    auto comment_idx = line.find('#');
+    auto op_and_args = comment_idx == std::string::npos ? line : line.substr(0, comment_idx);
+    op_and_args = android::base::Trim(op_and_args);
+    if (op_and_args.empty()) continue;
+
+    auto tokens = android::base::Split(op_and_args, " ");
+    const auto& op = tokens[0];
+    auto it = op_map.find(op);
+    if (it == op_map.end()) {
+      LOG(ERROR) << "Unknown operation in op_list: " << op;
+      return StringValue("");
+    }
+    OpParameters params;
+    params.tokens = tokens;
+    params.builder = builder.get();
+    if (!it->second(params)) {
+      return StringValue("");
+    }
+  }
+
+  auto metadata = builder->Export();
+  if (metadata == nullptr) {
+    LOG(ERROR) << "Failed to export metadata.";
+    return StringValue("");
+  }
+
+  if (!UpdatePartitionTable(super_device, *metadata, 0)) {
+    LOG(ERROR) << "Failed to write metadata.";
+    return StringValue("");
+  }
+
+  if (!SetUpdatedMarker(updated_marker)) {
+    LOG(ERROR) << "Failed to set metadata updated marker.";
+    return StringValue("");
+  }
+
+  return StringValue("t");
+}
+
+void RegisterDynamicPartitionsFunctions() {
+  RegisterFunction("unmap_partition", UnmapPartitionFn);
+  RegisterFunction("map_partition", MapPartitionFn);
+  RegisterFunction("update_dynamic_partitions", UpdateDynamicPartitionsFn);
+}
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/mounts.h b/updater/include/private/utils.h
similarity index 66%
copy from mounts.h
copy to updater/include/private/utils.h
index 0de1ebd..33cf615 100644
--- a/mounts.h
+++ b/updater/include/private/utils.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2019 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,8 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
 
-struct MountedVolume;
+#include <string>
 
-bool scan_mounted_volumes();
-
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+bool SetUpdatedMarker(const std::string& marker);
diff --git a/mounts.h b/updater/include/updater/dynamic_partitions.h
similarity index 66%
copy from mounts.h
copy to updater/include/updater/dynamic_partitions.h
index 0de1ebd..31cf859 100644
--- a/mounts.h
+++ b/updater/include/updater/dynamic_partitions.h
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2019 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,6 @@
  * limitations under the License.
  */
 
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
 
-struct MountedVolume;
-
-bool scan_mounted_volumes();
-
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+void RegisterDynamicPartitionsFunctions();
diff --git a/updater/install.cpp b/updater/install.cpp
index 9be7645..20a204a 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)
@@ -451,17 +393,20 @@
   return StringValue(mount_point);
 }
 
-static int exec_cmd(const char* path, char* const argv[]) {
+static int exec_cmd(const std::vector<std::string>& args) {
+  CHECK(!args.empty());
+  auto argv = StringVectorToNullTerminatedArray(args);
+
   pid_t child;
   if ((child = vfork()) == 0) {
-    execv(path, argv);
+    execv(argv[0], argv.data());
     _exit(EXIT_FAILURE);
   }
 
   int status;
   waitpid(child, &status, 0);
   if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
-    LOG(ERROR) << path << " failed with status " << WEXITSTATUS(status);
+    LOG(ERROR) << args[0] << " failed with status " << WEXITSTATUS(status);
   }
   return WEXITSTATUS(status);
 }
@@ -511,66 +456,53 @@
   }
 
   if (fs_type == "ext4") {
-    const char* mke2fs_argv[] = { "/sbin/mke2fs_static", "-t",    "ext4", "-b", "4096",
-                                  location.c_str(),      nullptr, nullptr };
-    std::string size_str;
+    std::vector<std::string> mke2fs_args = {
+      "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", location
+    };
     if (size != 0) {
-      size_str = std::to_string(size / 4096LL);
-      mke2fs_argv[6] = size_str.c_str();
+      mke2fs_args.push_back(std::to_string(size / 4096LL));
     }
 
-    int status = exec_cmd(mke2fs_argv[0], const_cast<char**>(mke2fs_argv));
-    if (status != 0) {
+    if (auto status = exec_cmd(mke2fs_args); status != 0) {
       LOG(ERROR) << name << ": mke2fs failed (" << status << ") on " << location;
       return StringValue("");
     }
 
-    const char* e2fsdroid_argv[] = { "/sbin/e2fsdroid_static", "-e",   "-a", mount_point.c_str(),
-                                     location.c_str(),         nullptr };
-    status = exec_cmd(e2fsdroid_argv[0], const_cast<char**>(e2fsdroid_argv));
-    if (status != 0) {
+    if (auto status = exec_cmd({ "/system/bin/e2fsdroid", "-e", "-a", mount_point, location });
+        status != 0) {
       LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location;
       return StringValue("");
     }
     return StringValue(location);
-  } else if (fs_type == "f2fs") {
+  }
+
+  if (fs_type == "f2fs") {
     if (size < 0) {
       LOG(ERROR) << name << ": fs_size can't be negative for f2fs: " << fs_size;
       return StringValue("");
     }
-    std::string num_sectors = std::to_string(size / 512);
-
-    const char* f2fs_path = "/sbin/mkfs.f2fs";
-    const char* f2fs_argv[] = { "mkfs.f2fs",
-                                "-d1",
-                                "-f",
-                                "-O", "encrypt",
-                                "-O", "quota",
-                                "-O", "verity",
-                                "-w", "512",
-                                location.c_str(),
-                                (size < 512) ? nullptr : num_sectors.c_str(),
-                                nullptr };
-    int status = exec_cmd(f2fs_path, const_cast<char**>(f2fs_argv));
-    if (status != 0) {
-      LOG(ERROR) << name << ": mkfs.f2fs failed (" << status << ") on " << location;
+    std::vector<std::string> f2fs_args = {
+      "/system/bin/make_f2fs", "-g", "android", "-w", "512", location
+    };
+    if (size >= 512) {
+      f2fs_args.push_back(std::to_string(size / 512));
+    }
+    if (auto status = exec_cmd(f2fs_args); status != 0) {
+      LOG(ERROR) << name << ": make_f2fs failed (" << status << ") on " << location;
       return StringValue("");
     }
 
-    const char* sload_argv[] = { "/sbin/sload.f2fs", "-t", mount_point.c_str(), location.c_str(),
-                                 nullptr };
-    status = exec_cmd(sload_argv[0], const_cast<char**>(sload_argv));
-    if (status != 0) {
-      LOG(ERROR) << name << ": sload.f2fs failed (" << status << ") on " << location;
+    if (auto status = exec_cmd({ "/system/bin/sload_f2fs", "-t", mount_point, location });
+        status != 0) {
+      LOG(ERROR) << name << ": sload_f2fs failed (" << status << ") on " << location;
       return StringValue("");
     }
 
     return StringValue(location);
-  } else {
-    LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \""
-               << partition_type << "\"";
   }
 
+  LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \""
+             << partition_type << "\"";
   return nullptr;
 }
 
@@ -661,33 +593,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 +644,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("");
@@ -758,17 +669,12 @@
     return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
   }
 
-  char* args2[argv.size() + 1];
-  for (size_t i = 0; i < argv.size(); i++) {
-    args2[i] = &args[i][0];
-  }
-  args2[argv.size()] = nullptr;
-
-  LOG(INFO) << "about to run program [" << args2[0] << "] with " << argv.size() << " args";
+  auto exec_args = StringVectorToNullTerminatedArray(args);
+  LOG(INFO) << "about to run program [" << exec_args[0] << "] with " << argv.size() << " args";
 
   pid_t child = fork();
   if (child == 0) {
-    execv(args2[0], args2);
+    execv(exec_args[0], exec_args.data());
     PLOG(ERROR) << "run_program: execv failed";
     _exit(EXIT_FAILURE);
   }
@@ -786,8 +692,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 +701,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 +778,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 +866,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);
@@ -991,20 +898,12 @@
     return ErrorAbort(state, kArgsParsingFailure, "%s() could not read args", name);
   }
 
-  char* args2[argv.size() + 1];
-  // Tune2fs expects the program name as its args[0]
-  args2[0] = const_cast<char*>(name);
-  if (args2[0] == nullptr) {
-    return nullptr;
-  }
-  for (size_t i = 0; i < argv.size(); ++i) {
-    args2[i + 1] = &args[i][0];
-  }
+  // tune2fs expects the program name as its first arg.
+  args.insert(args.begin(), "tune2fs");
+  auto tune2fs_args = StringVectorToNullTerminatedArray(args);
 
-  // tune2fs changes the file system parameters on an ext2 file system; it
-  // returns 0 on success.
-  int result = tune2fs_main(argv.size() + 1, args2);
-  if (result != 0) {
+  // tune2fs changes the filesystem parameters on an ext2 filesystem; it returns 0 on success.
+  if (auto result = tune2fs_main(tune2fs_args.size() - 1, tune2fs_args.data()); result != 0) {
     return ErrorAbort(state, kTune2FsFailure, "%s() returned error code %d", name, result);
   }
   return StringValue("t");
@@ -1022,14 +921,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..7b5a3f9 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,12 +31,11 @@
 #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/dynamic_partitions.h"
 #include "updater/install.h"
 
 // Generated by the makefile, this function defines the
@@ -48,8 +47,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 */,
@@ -129,13 +126,14 @@
   RegisterBuiltins();
   RegisterInstallFunctions();
   RegisterBlockImageFunctions();
+  RegisterDynamicPartitionsFunctions();
   RegisterDeviceExtensions();
 
   // Parse the script.
 
   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 +165,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 +200,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..0a25116
--- /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.PrepareUpdateService"/>
+    </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..bc66a9b
--- /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 -e class com.example.android.systemupdatersample.UpdateManagerTest#applyUpdate_appliesPayloadToUpdateEngine \
+     com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner
+   ```
+
+
+## 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..c02e608
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
@@ -0,0 +1,601 @@
+/*
+ * 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.Handler;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.services.PrepareUpdateService;
+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.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. */
+    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 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();
+
+    private final Handler mHandler;
+
+    /**
+     * @param updateEngine UpdateEngine instance.
+     * @param handler      Handler for {@link PrepareUpdateService} intent service.
+     */
+    public UpdateManager(UpdateEngine updateEngine, Handler handler) {
+        this.mUpdateEngine = updateEngine;
+        this.mHandler = handler;
+    }
+
+    /**
+     * 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);
+        }
+
+        Log.d(TAG, "Starting PrepareUpdateService");
+        PrepareUpdateService.startService(context, config, mHandler, (code, payloadSpec) -> {
+            if (code != PrepareUpdateService.RESULT_CODE_SUCCESS) {
+                Log.e(TAG, "PrepareUpdateService failed, result code is " + code);
+                setUpdaterStateSilent(UpdaterState.ERROR);
+                return;
+            }
+            updateEngineApplyPayload(UpdateData.builder()
+                    .setExtraProperties(prepareExtraProperties(config))
+                    .setPayload(payloadSpec)
+                    .build());
+        });
+    }
+
+    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);
+        }
+        if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_STREAMING) {
+            extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT);
+            config.getAbConfig()
+                    .getAuthorization()
+                    .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s));
+        }
+        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/PrepareUpdateService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java
new file mode 100644
index 0000000..29eb13d
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java
@@ -0,0 +1,309 @@
+/*
+ * 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.os.UpdateEngine;
+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.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+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.
+ *
+ * PrepareUpdateService runs on it's own thread. It will notify activity
+ * using interface {@link UpdateResultCallback} when update is ready to install.
+ */
+public class PrepareUpdateService extends IntentService {
+
+    /**
+     * UpdateResultCallback result codes.
+     */
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final int RESULT_CODE_ERROR = 1;
+
+    /**
+     * Extra params that will be sent to IntentService.
+     */
+    public static final String EXTRA_PARAM_CONFIG = "config";
+    public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
+
+    /**
+     * This interface is used to send results from {@link PrepareUpdateService} 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 PrepareUpdateService}
+         * @param payloadSpec prepared payload spec for streaming update
+         */
+        void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+    }
+
+    /**
+     * Starts PrepareUpdateService.
+     *
+     * @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,
+            Handler handler,
+            UpdateResultCallback resultCallback) {
+        Log.d(TAG, "Starting PrepareUpdateService");
+        ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback);
+        Intent intent = new Intent(context, PrepareUpdateService.class);
+        intent.putExtra(EXTRA_PARAM_CONFIG, config);
+        intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+        context.startService(intent);
+    }
+
+    public PrepareUpdateService() {
+        super(TAG);
+    }
+
+    private static final String TAG = "PrepareUpdateService";
+
+    /**
+     * 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();
+    private final UpdateEngine mUpdateEngine = new UpdateEngine();
+
+    @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 {
+
+        if (config.getAbConfig().getVerifyPayloadMetadata()) {
+            Log.i(TAG, "Verifying payload metadata with UpdateEngine.");
+            if (!verifyPayloadMetadata(config)) {
+                throw new PreparationFailedException("Payload metadata is not compatible");
+            }
+        }
+
+        if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
+            return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
+        }
+
+        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 only payload_metadata.bin and verifies with
+     * {@link UpdateEngine#verifyPayloadMetadata}.
+     * Returns {@code true} if the payload is verified or the result is unknown because of
+     * exception from UpdateEngine.
+     * By downloading only small portion of the package, it allows to verify if UpdateEngine
+     * will install the update.
+     */
+    private boolean verifyPayloadMetadata(UpdateConfig config) {
+        Optional<UpdateConfig.PackageFile> metadataPackageFile =
+                Arrays.stream(config.getAbConfig().getPropertyFiles())
+                        .filter(p -> p.getFilename().equals(
+                                PackageFiles.PAYLOAD_METADATA_FILE_NAME))
+                        .findFirst();
+        if (!metadataPackageFile.isPresent()) {
+            Log.w(TAG, String.format("ab_config.property_files doesn't contain %s",
+                    PackageFiles.PAYLOAD_METADATA_FILE_NAME));
+            return true;
+        }
+        Path metadataPath = Paths.get(OTA_PACKAGE_DIR, PackageFiles.PAYLOAD_METADATA_FILE_NAME);
+        try {
+            Files.deleteIfExists(metadataPath);
+            FileDownloader d = new FileDownloader(
+                    config.getUrl(),
+                    metadataPackageFile.get().getOffset(),
+                    metadataPackageFile.get().getSize(),
+                    metadataPath.toFile());
+            d.download();
+        } catch (IOException e) {
+            Log.w(TAG, String.format("Downloading %s from %s failed",
+                    PackageFiles.PAYLOAD_METADATA_FILE_NAME,
+                    config.getUrl()), e);
+            return true;
+        }
+        try {
+            return mUpdateEngine.verifyPayloadMetadata(metadataPath.toAbsolutePath().toString());
+        } catch (Exception e) {
+            Log.w(TAG, "UpdateEngine#verifyPayloadMetadata failed", e);
+            return true;
+        }
+    }
+
+    /**
+     * 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 PrepareUpdateService} 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..6d1e4c3
--- /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.Handler;
+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.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 Handler());
+
+    @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..0f9083d
--- /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 a 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..7867770
--- /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: "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..5ad16d4
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
@@ -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.
+ */
+
+package com.example.android.systemupdatersample;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+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.content.Intent;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.services.PrepareUpdateService;
+import com.example.android.systemupdatersample.tests.R;
+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.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 Context mMockContext;
+    private UpdateManager mSubject;
+    private Context mTestContext;
+    private UpdateConfig mStreamingUpdate002;
+
+    @Before
+    public void setUp() throws Exception {
+        mTestContext = InstrumentationRegistry.getContext();
+        mSubject = new UpdateManager(mUpdateEngine, null);
+        mStreamingUpdate002 =
+                UpdateConfig.fromJson(readResource(R.raw.update_config_002_stream));
+    }
+
+    @Test
+    public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception {
+        mockContextStartServiceAnswer(buildMockPayloadSpec());
+        mSubject.applyUpdate(mMockContext, mStreamingUpdate002);
+
+        verify(mUpdateEngine).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[]{
+                        "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false
+                        "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT
+                });
+    }
+
+    @Test
+    @UiThreadTest
+    public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Throwable {
+        mockContextStartServiceAnswer(buildMockPayloadSpec());
+        // UpdateEngine always returns IDLE status.
+        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(mMockContext, mStreamingUpdate002);
+        mSubject.unbind();
+        mSubject.bind(); // re-bind - now it should re-apply last update
+
+        assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING);
+        verify(mUpdateEngine, times(2)).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[]{
+                        "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false
+                        "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT
+                });
+    }
+
+    private void mockContextStartServiceAnswer(PayloadSpec payloadSpec) {
+        doAnswer(args -> {
+            Intent intent = args.getArgument(0);
+            ResultReceiver resultReceiver = intent.getParcelableExtra(
+                    PrepareUpdateService.EXTRA_PARAM_RESULT_RECEIVER);
+            Bundle b = new Bundle();
+            b.putSerializable(
+                    /* PrepareUpdateService.CallbackResultReceiver.BUNDLE_PARAM_PAYLOAD_SPEC */
+                    "payload-spec",
+                    payloadSpec);
+            resultReceiver.send(PrepareUpdateService.RESULT_CODE_SUCCESS, b);
+            return null;
+        }).when(mMockContext).startService(any(Intent.class));
+    }
+
+    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(
+                mTestContext.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/verifier.cpp b/verifier.cpp
index 283e043..44bd4e1 100644
--- a/verifier.cpp
+++ b/verifier.cpp
@@ -27,9 +27,14 @@
 #include <vector>
 
 #include <android-base/logging.h>
+#include <openssl/bio.h>
 #include <openssl/bn.h>
 #include <openssl/ecdsa.h>
+#include <openssl/evp.h>
 #include <openssl/obj_mac.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
+#include <ziparchive/zip_archive.h>
 
 #include "asn1_decoder.h"
 #include "otautil/print_sha1.h"
@@ -303,251 +308,160 @@
   return VERIFY_FAILURE;
 }
 
-std::unique_ptr<RSA, RSADeleter> parse_rsa_key(FILE* file, uint32_t exponent) {
-    // Read key length in words and n0inv. n0inv is a precomputed montgomery
-    // parameter derived from the modulus and can be used to speed up
-    // verification. n0inv is 32 bits wide here, assuming the verification logic
-    // uses 32 bit arithmetic. However, BoringSSL may use a word size of 64 bits
-    // internally, in which case we don't have a valid n0inv. Thus, we just
-    // ignore the montgomery parameters and have BoringSSL recompute them
-    // internally. If/When the speedup from using the montgomery parameters
-    // becomes relevant, we can add more sophisticated code here to obtain a
-    // 64-bit n0inv and initialize the montgomery parameters in the key object.
-    uint32_t key_len_words = 0;
-    uint32_t n0inv = 0;
-    if (fscanf(file, " %i , 0x%x", &key_len_words, &n0inv) != 2) {
-        return nullptr;
-    }
-
-    if (key_len_words > 8192 / 32) {
-        LOG(ERROR) << "key length (" << key_len_words << ") too large";
-        return nullptr;
-    }
-
-    // Read the modulus.
-    std::unique_ptr<uint32_t[]> modulus(new uint32_t[key_len_words]);
-    if (fscanf(file, " , { %u", &modulus[0]) != 1) {
-        return nullptr;
-    }
-    for (uint32_t i = 1; i < key_len_words; ++i) {
-        if (fscanf(file, " , %u", &modulus[i]) != 1) {
-            return nullptr;
-        }
-    }
-
-    // Cconvert from little-endian array of little-endian words to big-endian
-    // byte array suitable as input for BN_bin2bn.
-    std::reverse((uint8_t*)modulus.get(),
-                 (uint8_t*)(modulus.get() + key_len_words));
-
-    // The next sequence of values is the montgomery parameter R^2. Since we
-    // generally don't have a valid |n0inv|, we ignore this (see comment above).
-    uint32_t rr_value;
-    if (fscanf(file, " } , { %u", &rr_value) != 1) {
-        return nullptr;
-    }
-    for (uint32_t i = 1; i < key_len_words; ++i) {
-        if (fscanf(file, " , %u", &rr_value) != 1) {
-            return nullptr;
-        }
-    }
-    if (fscanf(file, " } } ") != 0) {
-        return nullptr;
-    }
-
-    // Initialize the key.
-    std::unique_ptr<RSA, RSADeleter> key(RSA_new());
-    if (!key) {
-      return nullptr;
-    }
-
-    key->n = BN_bin2bn((uint8_t*)modulus.get(),
-                       key_len_words * sizeof(uint32_t), NULL);
-    if (!key->n) {
-      return nullptr;
-    }
-
-    key->e = BN_new();
-    if (!key->e || !BN_set_word(key->e, exponent)) {
-      return nullptr;
-    }
-
-    return key;
-}
-
-struct BNDeleter {
-  void operator()(BIGNUM* bn) const {
-    BN_free(bn);
+static std::vector<Certificate> IterateZipEntriesAndSearchForKeys(const ZipArchiveHandle& handle) {
+  void* cookie;
+  ZipString suffix("x509.pem");
+  int32_t iter_status = StartIteration(handle, &cookie, nullptr, &suffix);
+  if (iter_status != 0) {
+    LOG(ERROR) << "Failed to iterate over entries in the certificate zipfile: "
+               << ErrorCodeString(iter_status);
+    return {};
   }
-};
 
-std::unique_ptr<EC_KEY, ECKEYDeleter> parse_ec_key(FILE* file) {
-    uint32_t key_len_bytes = 0;
-    if (fscanf(file, " %i", &key_len_bytes) != 1) {
-        return nullptr;
+  std::vector<Certificate> result;
+
+  ZipString name;
+  ZipEntry entry;
+  while ((iter_status = Next(cookie, &entry, &name)) == 0) {
+    std::vector<uint8_t> pem_content(entry.uncompressed_length);
+    if (int32_t extract_status =
+            ExtractToMemory(handle, &entry, pem_content.data(), pem_content.size());
+        extract_status != 0) {
+      LOG(ERROR) << "Failed to extract " << std::string(name.name, name.name + name.name_length);
+      return {};
     }
 
-    std::unique_ptr<EC_GROUP, void (*)(EC_GROUP*)> group(
-        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1), EC_GROUP_free);
-    if (!group) {
-        return nullptr;
+    Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+    // Aborts the parsing if we fail to load one of the key file.
+    if (!LoadCertificateFromBuffer(pem_content, &cert)) {
+      LOG(ERROR) << "Failed to load keys from "
+                 << std::string(name.name, name.name + name.name_length);
+      return {};
     }
 
-    // Verify that |key_len| matches the group order.
-    if (key_len_bytes != BN_num_bytes(EC_GROUP_get0_order(group.get()))) {
-        return nullptr;
-    }
+    result.emplace_back(std::move(cert));
+  }
 
-    // Read the public key coordinates. Note that the byte order in the file is
-    // little-endian, so we convert to big-endian here.
-    std::unique_ptr<uint8_t[]> bytes(new uint8_t[key_len_bytes]);
-    std::unique_ptr<BIGNUM, BNDeleter> point[2];
-    for (int i = 0; i < 2; ++i) {
-        unsigned int byte = 0;
-        if (fscanf(file, " , { %u", &byte) != 1) {
-            return nullptr;
-        }
-        bytes[key_len_bytes - 1] = byte;
+  if (iter_status != -1) {
+    LOG(ERROR) << "Error while iterating over zip entries: " << ErrorCodeString(iter_status);
+    return {};
+  }
 
-        for (size_t i = 1; i < key_len_bytes; ++i) {
-            if (fscanf(file, " , %u", &byte) != 1) {
-                return nullptr;
-            }
-            bytes[key_len_bytes - i - 1] = byte;
-        }
-
-        point[i].reset(BN_bin2bn(bytes.get(), key_len_bytes, nullptr));
-        if (!point[i]) {
-            return nullptr;
-        }
-
-        if (fscanf(file, " }") != 0) {
-            return nullptr;
-        }
-    }
-
-    if (fscanf(file, " } ") != 0) {
-        return nullptr;
-    }
-
-    // Create and initialize the key.
-    std::unique_ptr<EC_KEY, ECKEYDeleter> key(EC_KEY_new());
-    if (!key || !EC_KEY_set_group(key.get(), group.get()) ||
-        !EC_KEY_set_public_key_affine_coordinates(key.get(), point[0].get(),
-                                                  point[1].get())) {
-        return nullptr;
-    }
-
-    return key;
+  return result;
 }
 
-// Reads a file containing one or more public keys as produced by
-// DumpPublicKey:  this is an RSAPublicKey struct as it would appear
-// as a C source literal, eg:
-//
-//  "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"
-//
-// For key versions newer than the original 2048-bit e=3 keys
-// supported by Android, the string is preceded by a version
-// identifier, eg:
-//
-//  "v2 {64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}"
-//
-// (Note that the braces and commas in this example are actual
-// characters the parser expects to find in the file; the ellipses
-// indicate more numbers omitted from this example.)
-//
-// The file may contain multiple keys in this format, separated by
-// commas.  The last key must not be followed by a comma.
-//
-// A Certificate is a pair of an RSAPublicKey and a particular hash
-// (we support SHA-1 and SHA-256; we store the hash length to signify
-// which is being used).  The hash used is implied by the version number.
-//
-//       1: 2048-bit RSA key with e=3 and SHA-1 hash
-//       2: 2048-bit RSA key with e=65537 and SHA-1 hash
-//       3: 2048-bit RSA key with e=3 and SHA-256 hash
-//       4: 2048-bit RSA key with e=65537 and SHA-256 hash
-//       5: 256-bit EC key using the NIST P-256 curve parameters and SHA-256 hash
-//
-// Returns true on success, and appends the found keys (at least one) to certs.
-// Otherwise returns false if the file failed to parse, or if it contains zero
-// keys. The contents in certs would be unspecified on failure.
-bool load_keys(const char* filename, std::vector<Certificate>& certs) {
-  std::unique_ptr<FILE, decltype(&fclose)> f(fopen(filename, "re"), fclose);
-  if (!f) {
-    PLOG(ERROR) << "error opening " << filename;
+std::vector<Certificate> LoadKeysFromZipfile(const std::string& zip_name) {
+  ZipArchiveHandle handle;
+  if (int32_t open_status = OpenArchive(zip_name.c_str(), &handle); open_status != 0) {
+    LOG(ERROR) << "Failed to open " << zip_name << ": " << ErrorCodeString(open_status);
+    return {};
+  }
+
+  std::vector<Certificate> result = IterateZipEntriesAndSearchForKeys(handle);
+  CloseArchive(handle);
+  return result;
+}
+
+bool CheckRSAKey(const std::unique_ptr<RSA, RSADeleter>& rsa) {
+  if (!rsa) {
     return false;
   }
 
-  while (true) {
-    certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
-    Certificate& cert = certs.back();
-    uint32_t exponent = 0;
-
-    char start_char;
-    if (fscanf(f.get(), " %c", &start_char) != 1) return false;
-    if (start_char == '{') {
-      // a version 1 key has no version specifier.
-      cert.key_type = Certificate::KEY_TYPE_RSA;
-      exponent = 3;
-      cert.hash_len = SHA_DIGEST_LENGTH;
-    } else if (start_char == 'v') {
-      int version;
-      if (fscanf(f.get(), "%d {", &version) != 1) return false;
-      switch (version) {
-        case 2:
-          cert.key_type = Certificate::KEY_TYPE_RSA;
-          exponent = 65537;
-          cert.hash_len = SHA_DIGEST_LENGTH;
-          break;
-        case 3:
-          cert.key_type = Certificate::KEY_TYPE_RSA;
-          exponent = 3;
-          cert.hash_len = SHA256_DIGEST_LENGTH;
-          break;
-        case 4:
-          cert.key_type = Certificate::KEY_TYPE_RSA;
-          exponent = 65537;
-          cert.hash_len = SHA256_DIGEST_LENGTH;
-          break;
-        case 5:
-          cert.key_type = Certificate::KEY_TYPE_EC;
-          cert.hash_len = SHA256_DIGEST_LENGTH;
-          break;
-        default:
-          return false;
-      }
-    }
-
-    if (cert.key_type == Certificate::KEY_TYPE_RSA) {
-      cert.rsa = parse_rsa_key(f.get(), exponent);
-      if (!cert.rsa) {
-        return false;
-      }
-
-      LOG(INFO) << "read key e=" << exponent << " hash=" << cert.hash_len;
-    } else if (cert.key_type == Certificate::KEY_TYPE_EC) {
-      cert.ec = parse_ec_key(f.get());
-      if (!cert.ec) {
-        return false;
-      }
-    } else {
-      LOG(ERROR) << "Unknown key type " << cert.key_type;
-      return false;
-    }
-
-    // if the line ends in a comma, this file has more keys.
-    int ch = fgetc(f.get());
-    if (ch == ',') {
-      // more keys to come.
-      continue;
-    } else if (ch == EOF) {
-      break;
-    } else {
-      LOG(ERROR) << "unexpected character between keys";
-      return false;
-    }
+  const BIGNUM* out_n;
+  const BIGNUM* out_e;
+  RSA_get0_key(rsa.get(), &out_n, &out_e, nullptr /* private exponent */);
+  auto modulus_bits = BN_num_bits(out_n);
+  if (modulus_bits != 2048) {
+    LOG(ERROR) << "Modulus should be 2048 bits long, actual: " << modulus_bits;
+    return false;
   }
+
+  BN_ULONG exponent = BN_get_word(out_e);
+  if (exponent != 3 && exponent != 65537) {
+    LOG(ERROR) << "Public exponent should be 3 or 65537, actual: " << exponent;
+    return false;
+  }
+
+  return true;
+}
+
+bool CheckECKey(const std::unique_ptr<EC_KEY, ECKEYDeleter>& ec_key) {
+  if (!ec_key) {
+    return false;
+  }
+
+  const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key.get());
+  if (!ec_group) {
+    LOG(ERROR) << "Failed to get the ec_group from the ec_key";
+    return false;
+  }
+  auto degree = EC_GROUP_get_degree(ec_group);
+  if (degree != 256) {
+    LOG(ERROR) << "Field size of the ec key should be 256 bits long, actual: " << degree;
+    return false;
+  }
+
+  return true;
+}
+
+bool LoadCertificateFromBuffer(const std::vector<uint8_t>& pem_content, Certificate* cert) {
+  std::unique_ptr<BIO, decltype(&BIO_free)> content(
+      BIO_new_mem_buf(pem_content.data(), pem_content.size()), BIO_free);
+
+  std::unique_ptr<X509, decltype(&X509_free)> x509(
+      PEM_read_bio_X509(content.get(), nullptr, nullptr, nullptr), X509_free);
+  if (!x509) {
+    LOG(ERROR) << "Failed to read x509 certificate";
+    return false;
+  }
+
+  int nid = X509_get_signature_nid(x509.get());
+  switch (nid) {
+    // SignApk has historically accepted md5WithRSA certificates, but treated them as
+    // sha1WithRSA anyway. Continue to do so for backwards compatibility.
+    case NID_md5WithRSA:
+    case NID_md5WithRSAEncryption:
+    case NID_sha1WithRSA:
+    case NID_sha1WithRSAEncryption:
+      cert->hash_len = SHA_DIGEST_LENGTH;
+      break;
+    case NID_sha256WithRSAEncryption:
+    case NID_ecdsa_with_SHA256:
+      cert->hash_len = SHA256_DIGEST_LENGTH;
+      break;
+    default:
+      LOG(ERROR) << "Unrecognized signature nid " << OBJ_nid2ln(nid);
+      return false;
+  }
+
+  std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> public_key(X509_get_pubkey(x509.get()),
+                                                                 EVP_PKEY_free);
+  if (!public_key) {
+    LOG(ERROR) << "Failed to extract the public key from x509 certificate";
+    return false;
+  }
+
+  int key_type = EVP_PKEY_id(public_key.get());
+  if (key_type == EVP_PKEY_RSA) {
+    cert->key_type = Certificate::KEY_TYPE_RSA;
+    cert->ec.reset();
+    cert->rsa.reset(EVP_PKEY_get1_RSA(public_key.get()));
+    if (!cert->rsa || !CheckRSAKey(cert->rsa)) {
+      LOG(ERROR) << "Failed to validate the rsa key info from public key";
+      return false;
+    }
+  } else if (key_type == EVP_PKEY_EC) {
+    cert->key_type = Certificate::KEY_TYPE_EC;
+    cert->rsa.reset();
+    cert->ec.reset(EVP_PKEY_get1_EC_KEY(public_key.get()));
+    if (!cert->ec || !CheckECKey(cert->ec)) {
+      LOG(ERROR) << "Failed to validate the ec key info from the public key";
+      return false;
+    }
+  } else {
+    LOG(ERROR) << "Unrecognized public key type " << OBJ_nid2ln(key_type);
+    return false;
+  }
+
   return true;
 }
diff --git a/verifier.h b/verifier.h
index 6fa8f2b..df9a4b6 100644
--- a/verifier.h
+++ b/verifier.h
@@ -17,6 +17,8 @@
 #ifndef _RECOVERY_VERIFIER_H
 #define _RECOVERY_VERIFIER_H
 
+#include <stdint.h>
+
 #include <functional>
 #include <memory>
 #include <vector>
@@ -68,7 +70,19 @@
 int verify_file(const unsigned char* addr, size_t length, const std::vector<Certificate>& keys,
                 const std::function<void(float)>& set_progress = nullptr);
 
-bool load_keys(const char* filename, std::vector<Certificate>& certs);
+// Checks that the RSA key has a modulus of 2048 bits long, and public exponent is 3 or 65537.
+bool CheckRSAKey(const std::unique_ptr<RSA, RSADeleter>& rsa);
+
+// Checks that the field size of the curve for the EC key is 256 bits.
+bool CheckECKey(const std::unique_ptr<EC_KEY, ECKEYDeleter>& ec_key);
+
+// Parses a PEM-encoded x509 certificate from the given buffer and saves it into |cert|. Returns
+// false if there is a parsing failure or the signature's encryption algorithm is not supported.
+bool LoadCertificateFromBuffer(const std::vector<uint8_t>& pem_content, Certificate* cert);
+
+// Iterates over the zip entries with the suffix "x509.pem" and returns a list of recognized
+// certificates. Returns an empty list if we fail to parse any of the entries.
+std::vector<Certificate> LoadKeysFromZipfile(const std::string& zip_name);
 
 #define VERIFY_SUCCESS        0
 #define VERIFY_FAILURE        1
diff --git a/vr_ui.cpp b/vr_ui.cpp
index a58c99e..1f0292c 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;
@@ -28,38 +34,39 @@
   return gr_fb_height();
 }
 
-void VrRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
+void VrRecoveryUI::DrawSurface(const 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);
+void VrRecoveryUI::DrawTextIcon(int x, int y, const GRSurface* surface) const {
+  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..2e8ac59 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,17 +28,18 @@
  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;
 
-  void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const override;
+  void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                   int dy) const override;
   int DrawHorizontalRule(int y) const override;
   void DrawHighlightBar(int x, int y, int width, int height) const override;
   void DrawFill(int x, int y, int w, int h) const override;
-  void DrawTextIcon(int x, int y, GRSurface* surface) const override;
-  int DrawTextLine(int x, int y, const char* line, bool bold) const override;
+  void DrawTextIcon(int x, int y, const GRSurface* surface) 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..6da84c9 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.
@@ -51,96 +51,39 @@
   gr_color(0, 0, 0, 255);
   gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
-  if (currentIcon != NONE) {
-    GRSurface* frame = GetCurrentFrame();
+  if (current_icon_ != NONE) {
+    const auto& frame = GetCurrentFrame();
     int frame_width = gr_get_width(frame);
     int frame_height = gr_get_height(frame);
     int frame_x = (gr_fb_width() - frame_width) / 2;
     int frame_y = (gr_fb_height() - frame_height) / 2;
     gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
+
+    // Draw recovery text on screen above progress bar.
+    const auto& 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();
   } else {
-    SetColor(TEXT_FILL);
+    SetColor(UIElement::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,14 @@
 
 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);
+std::unique_ptr<Menu> WearRecoveryUI::CreateMenu(const std::vector<std::string>& text_headers,
+                                                 const std::vector<std::string>& text_items,
+                                                 size_t initial_selection) const {
   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;
-    update_screen_locked();
+    return std::make_unique<TextMenu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1,
+                                      text_cols_ - 1, text_headers, text_items, initial_selection,
+                                      char_height_, *this);
   }
-  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;
+  return nullptr;
 }
diff --git a/wear_ui.h b/wear_ui.h
index 739b4cb..429af69 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,17 @@
 
   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_;
+
+  std::unique_ptr<Menu> CreateMenu(const std::vector<std::string>& text_headers,
+                                   const std::vector<std::string>& text_items,
+                                   size_t initial_selection) const override;
 
   int GetProgressBaseline() const override;
 
@@ -45,8 +47,6 @@
  private:
   void draw_background_locked() override;
   void draw_screen_locked() override;
-
-  int menu_start, menu_end;
 };
 
 #endif  // RECOVERY_WEAR_UI_H