Merge "logical -> dynamic partitions."
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index 1084291..28aa06f 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -7,5 +7,4 @@
 
 [Hook Scripts]
 checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-                  -fw updater_sample/
-
+                  --file_whitelist tools/ updater_sample/
diff --git a/install.cpp b/install.cpp
index 42d2641..680937d 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>
 
@@ -47,7 +45,6 @@
 #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/error_code.h"
@@ -67,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";
@@ -90,101 +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";
   }
 }
 
-// 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
+// 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);
+  }
+
+  auto target_build = get_value(metadata, "post-build-incremental");
+  if (!target_build.empty()) {
+    log_buffer->push_back("target_build: " + target_build);
+  }
+}
+
+// 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 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);
-    }
-  }
-
-  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;
-  }
-
-  // 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;
-  }
-
+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,13 +178,55 @@
   return 0;
 }
 
+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.
@@ -311,20 +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);
-
-  bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false);
   std::vector<std::string> args;
-  int ret = is_ab ? SetUpAbUpdateCommands(package, zip, pipefd[1], &args)
-                  : SetUpNonAbUpdateCommands(package, zip, retry_count, pipefd[1], &args);
-  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
@@ -477,9 +497,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;
diff --git a/install.h b/install.h
index 1d3d0cd..c6db1d1 100644
--- a/install.h
+++ b/install.h
@@ -19,6 +19,7 @@
 
 #include <stddef.h>
 
+#include <map>
 #include <string>
 
 #include <ziparchive/zip_archive.h>
@@ -33,6 +34,12 @@
   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, bool needs_mount,
@@ -42,12 +49,17 @@
 // 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/minadbd/Android.bp b/minadbd/Android.bp
index 370232b..7e33261 100644
--- a/minadbd/Android.bp
+++ b/minadbd/Android.bp
@@ -15,6 +15,7 @@
 cc_defaults {
     name: "minadbd_defaults",
 
+    cpp_std: "gnu++17",
     cflags: [
         "-DADB_HOST=0",
         "-Wall",
diff --git a/minui/graphics_adf.cpp b/minui/graphics_adf.cpp
index 6fc193f..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() {}
+GRSurfaceAdf::~GRSurfaceAdf() {
+  if (mmapped_buffer_) {
+    munmap(mmapped_buffer_, pitch * height);
+  }
+  if (fence_fd != -1) {
+    close(fence_fd);
+  }
+  if (fd != -1) {
+    close(fd);
+  }
+}
 
-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;
+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;
   }
 
-  surf->width = mode->hdisplay;
-  surf->height = mode->vdisplay;
-  surf->row_bytes = surf->pitch;
-  surf->pixel_bytes = (format == DRM_FORMAT_RGB565) ? 2 : 4;
+  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) {
-    int saved_errno = errno;
-    close(surf->fd);
-    return -saved_errno;
+    *err = -errno;
+    return nullptr;
   }
   surf->mmapped_buffer_ = static_cast<uint8_t*>(mmapped);
-  return 0;
+  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) {
@@ -153,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");
     }
@@ -169,33 +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) {
-  if (surf->mmapped_buffer_) {
-    munmap(surf->mmapped_buffer_, 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 099d329..bf98428 100644
--- a/minui/graphics_adf.h
+++ b/minui/graphics_adf.h
@@ -17,6 +17,9 @@
 #pragma once
 
 #include <stdint.h>
+#include <sys/types.h>
+
+#include <memory>
 
 #include <adf/adf.h>
 
@@ -25,6 +28,11 @@
 
 class GRSurfaceAdf : public GRSurface {
  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_;
   }
@@ -32,34 +40,36 @@
  private:
   friend class MinuiBackendAdf;
 
-  int fence_fd;
-  int fd;
-  __u32 offset;
-  __u32 pitch;
+  GRSurfaceAdf(int width, int height, int row_bytes, int 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];
 };
diff --git a/minui/graphics_drm.cpp b/minui/graphics_drm.cpp
index 81b49fd..765e262 100644
--- a/minui/graphics_drm.cpp
+++ b/minui/graphics_drm.cpp
@@ -24,74 +24,37 @@
 #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
-  }
-}
-
-int MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface) {
-  int ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0,  // x,y
-                           &main_monitor_connector->connector_id,
-                           1,  // connector_count
-                           &main_monitor_crtc->mode);
-
-  if (ret) {
-    printf("drmModeSetCrtc failed ret=%d\n", ret);
+GRSurfaceDrm::~GRSurfaceDrm() {
+  if (mmapped_buffer_) {
+    munmap(mmapped_buffer_, row_bytes * height);
   }
 
-  return 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->mmapped_buffer_) {
-    munmap(surface->mmapped_buffer_, surface->row_bytes * surface->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) {
@@ -111,10 +74,7 @@
   }
 }
 
-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;
   PixelFormat pixel_format = gr_pixel_format();
   // PixelFormat comes in byte order, whereas DRM_FORMAT_* uses little-endian
@@ -137,52 +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;
   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("mmap() failed");
-    DrmDestroySurface(surface);
+    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.
@@ -264,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) {
@@ -298,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;
@@ -366,21 +341,20 @@
 
   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;
 
   // We will likely encounter errors in the backend functions (i.e. Flip) if EnableCrtc fails.
-  if (DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]) != 0) {
+  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,
@@ -393,12 +367,9 @@
 
 GRSurface* MinuiBackendDrm::Flip() {
   bool ongoing_flip = true;
-
-  int ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id,
-                            GRSurfaceDrms[current_buffer]->fb_id,
-                            DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip);
-  if (ret < 0) {
-    printf("drmModePageFlip failed ret=%d\n", ret);
+  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;
   }
 
@@ -408,9 +379,8 @@
       .events = POLLIN
     };
 
-    ret = poll(&fds, 1, -1);
-    if (ret == -1 || !(fds.revents & POLLIN)) {
-      printf("poll() failed on drm fd\n");
+    if (poll(&fds, 1, -1) == -1 || !(fds.revents & POLLIN)) {
+      perror("Failed to poll() on drm fd");
       break;
     }
 
@@ -419,21 +389,18 @@
       .page_flip_handler = page_flip_complete
     };
 
-    ret = drmHandleEvent(drm_fd, &evctx);
-    if (ret != 0) {
-      printf("drmHandleEvent failed ret=%d\n", ret);
+    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 f3aad6b..6ba46e6 100644
--- a/minui/graphics_drm.h
+++ b/minui/graphics_drm.h
@@ -18,6 +18,8 @@
 
 #include <stdint.h>
 
+#include <memory>
+
 #include <xf86drmMode.h>
 
 #include "graphics.h"
@@ -25,6 +27,11 @@
 
 class GRSurfaceDrm : public GRSurface {
  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_;
   }
@@ -32,30 +39,34 @@
  private:
   friend class MinuiBackendDrm;
 
-  uint32_t fb_id;
-  uint32_t handle;
+  GRSurfaceDrm(int width, int height, int row_bytes, int 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);
-  int 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 };
 };
diff --git a/minui/graphics_fbdev.cpp b/minui/graphics_fbdev.cpp
index f958d62..93e4420 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(int width, int height, int row_bytes,
+                                                       int 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,46 +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].buffer_ = static_cast<uint8_t*>(bits);
-  memset(gr_framebuffer[0].buffer_, 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;
 
-    gr_framebuffer[1] = gr_framebuffer[0];
-    gr_framebuffer[1].buffer_ =
-        gr_framebuffer[0].buffer_ + 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 = new GRSurfaceFbdev;
-    *gr_draw = gr_framebuffer[0];
-    gr_draw->buffer_ = new uint8_t[gr_draw->height * gr_draw->row_bytes];
+    // 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();
   }
 
+  gr_draw = gr_framebuffer[1].get();
   memset(gr_draw->buffer_, 0, gr_draw->height * gr_draw->row_bytes);
-  fb_fd = fd;
+  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 (%d x %d)\n", fb_fd.get(), gr_draw->width, gr_draw->height);
 
   Blank(true);
   Blank(false);
@@ -139,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].buffer_, gr_draw->buffer_, 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) {
-    delete[] gr_draw->buffer_;
-    delete gr_draw;
-  }
-  gr_draw = nullptr;
-}
diff --git a/minui/graphics_fbdev.h b/minui/graphics_fbdev.h
index be813dc..016ab88 100644
--- a/minui/graphics_fbdev.h
+++ b/minui/graphics_fbdev.h
@@ -19,37 +19,52 @@
 #include <linux/fb.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(int width, int height, int row_bytes,
+                                                int 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_;
+  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);
 
-  GRSurfaceFbdev 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;
-  GRSurfaceFbdev* 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;
 };
diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h
index e9bd1c4..3231248 100644
--- a/minui/include/minui/minui.h
+++ b/minui/include/minui/minui.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <stdint.h>
+#include <stdlib.h>
 #include <sys/types.h>
 
 #include <functional>
@@ -24,22 +25,27 @@
 #include <string>
 #include <vector>
 
+#include <android-base/macros.h>
+
 //
 // Graphics.
 //
 
 class GRSurface {
  public:
-  GRSurface() = default;
-  virtual ~GRSurface();
+  virtual ~GRSurface() = default;
 
-  // Creates and returns a GRSurface instance for the given data_size. The starting address of the
-  // surface data is aligned to SURFACE_DATA_ALIGNMENT. Returns the created GRSurface instance (in
-  // std::unique_ptr), or nullptr on error.
-  static std::unique_ptr<GRSurface> Create(size_t data_size);
+  // Creates and returns a GRSurface instance that's sufficient for storing an image of the given
+  // size. The starting address of the surface data is aligned to SURFACE_DATA_ALIGNMENT. Returns
+  // the created GRSurface instance (in std::unique_ptr), or nullptr on error.
+  static std::unique_ptr<GRSurface> Create(int width, int height, int row_bytes, int pixel_bytes,
+                                           size_t data_size);
+
+  // Clones the current GRSurface instance (i.e. an image).
+  std::unique_ptr<GRSurface> Clone() const;
 
   virtual uint8_t* data() {
-    return data_;
+    return data_.get();
   }
 
   const uint8_t* data() const {
@@ -51,8 +57,22 @@
   int row_bytes;
   int pixel_bytes;
 
+ protected:
+  GRSurface(int width, int height, int row_bytes, int pixel_bytes)
+      : width(width), height(height), row_bytes(row_bytes), pixel_bytes(pixel_bytes) {}
+
  private:
-  uint8_t* data_{ nullptr };
+  // 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 {
diff --git a/minui/resources.cpp b/minui/resources.cpp
index c01c186..c7af190 100644
--- a/minui/resources.cpp
+++ b/minui/resources.cpp
@@ -39,21 +39,24 @@
 
 static std::string g_resource_dir{ "/res/images" };
 
-std::unique_ptr<GRSurface> GRSurface::Create(size_t data_size) {
+std::unique_ptr<GRSurface> GRSurface::Create(int width, int height, int row_bytes, int pixel_bytes,
+                                             size_t data_size) {
   static constexpr size_t kSurfaceDataAlignment = 8;
-  std::unique_ptr<GRSurface> result = std::make_unique<GRSurface>();
-  size_t aligned_size =
+  // Cannot use std::make_unique to access non-public ctor.
+  auto result = std::unique_ptr<GRSurface>(new GRSurface(width, height, row_bytes, pixel_bytes));
+  result->data_size_ =
       (data_size + kSurfaceDataAlignment - 1) / kSurfaceDataAlignment * kSurfaceDataAlignment;
-  result->data_ = static_cast<uint8_t*>(aligned_alloc(kSurfaceDataAlignment, aligned_size));
-  if (result->data_ == nullptr) return nullptr;
+  result->data_.reset(
+      static_cast<uint8_t*>(aligned_alloc(kSurfaceDataAlignment, result->data_size_)));
+  if (!result->data_) return nullptr;
   return result;
 }
 
-GRSurface::~GRSurface() {
-  if (data_ != nullptr) {
-    free(data_);
-    data_ = nullptr;
-  }
+std::unique_ptr<GRSurface> GRSurface::Clone() const {
+  auto result = GRSurface::Create(width, height, row_bytes, pixel_bytes, data_size_);
+  if (!result) return nullptr;
+  memcpy(result->data(), data(), data_size_);
+  return result;
 }
 
 PngHandler::PngHandler(const std::string& name) {
@@ -68,7 +71,7 @@
     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;
@@ -131,70 +134,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.
 
-// Allocates and returns a GRSurface* sufficient for storing an image of the indicated size in the
-// framebuffer pixel format.
-static std::unique_ptr<GRSurface> init_display_surface(png_uint_32 width, png_uint_32 height) {
-  std::unique_ptr<GRSurface> surface = GRSurface::Create(width * height * 4);
-  if (!surface) return nullptr;
-
-  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) {
@@ -207,7 +189,7 @@
   png_uint_32 width = png_handler.width();
   png_uint_32 height = png_handler.height();
 
-  std::unique_ptr<GRSurface> surface = init_display_surface(width, height);
+  auto surface = GRSurface::Create(width, height, width * 4, 4, width * height * 4);
   if (!surface) {
     return -8;
   }
@@ -218,10 +200,10 @@
   }
 
   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);
-    transform_rgb_to_draw(p_row.data(), surface->data() + y * surface->row_bytes,
-                          png_handler.channels(), width);
+    TransformRgbToDraw(p_row.data(), surface->data() + y * surface->row_bytes,
+                       png_handler.channels(), width);
   }
 
   *pSurface = surface.release();
@@ -277,7 +259,9 @@
     goto exit;
   }
   for (int i = 0; i < *frames; ++i) {
-    auto created_surface = init_display_surface(width, height / *frames);
+    auto height_per_frame = height / *frames;
+    auto created_surface =
+        GRSurface::Create(width, height_per_frame, width * 4, 4, width * height_per_frame);
     if (!created_surface) {
       result = -8;
       goto exit;
@@ -290,11 +274,11 @@
   }
 
   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;
@@ -325,14 +309,10 @@
   png_uint_32 width = png_handler.width();
   png_uint_32 height = png_handler.height();
 
-  std::unique_ptr<GRSurface> surface = GRSurface::Create(width * height);
+  auto surface = GRSurface::Create(width, height, width, 1, width * height);
   if (!surface) {
     return -8;
   }
-  surface->width = width;
-  surface->height = height;
-  surface->row_bytes = width;
-  surface->pixel_bytes = 1;
 
   PixelFormat pixel_format = gr_pixel_format();
   if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
@@ -340,7 +320,7 @@
   }
 
   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);
   }
 
@@ -389,7 +369,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];
@@ -425,7 +405,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];
@@ -435,14 +415,10 @@
     if (y + 1 + h >= height || matches_locale(loc, locale)) {
       printf("  %20s: %s (%d x %d @ %d)\n", name, loc, w, h, y);
 
-      std::unique_ptr<GRSurface> surface = GRSurface::Create(w * h);
+      auto surface = GRSurface::Create(w, h, w, 1, w * h);
       if (!surface) {
         return -8;
       }
-      surface->width = w;
-      surface->height = h;
-      surface->row_bytes = w;
-      surface->pixel_bytes = 1;
 
       for (int i = 0; i < h; ++i, ++y) {
         png_read_row(png_ptr, row.data(), nullptr);
diff --git a/recovery.cpp b/recovery.cpp
index d7bc6fd..e17526a 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -520,34 +520,17 @@
         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;
 }
 
 // Wipes the current A/B device, with a secure wipe of all the partitions in RECOVERY_WIPE.
diff --git a/recovery_main.cpp b/recovery_main.cpp
index a95299e..6f50802 100644
--- a/recovery_main.cpp
+++ b/recovery_main.cpp
@@ -478,8 +478,13 @@
         break;
 
       case Device::ENTER_FASTBOOT:
-        LOG(INFO) << "Entering fastboot";
-        fastboot = true;
+        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:
diff --git a/roots.cpp b/roots.cpp
index c29771a..dc34784 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -38,10 +38,12 @@
 #include <cryptfs.h>
 #include <ext4_utils/wipe.h>
 #include <fs_mgr.h>
+#include <fs_mgr_dm_linear.h>
 
 #include "otautil/mounts.h"
 
 static struct fstab* fstab = nullptr;
+static bool did_map_logical_partitions = false;
 
 extern struct selabel_handle* sehandle;
 
@@ -117,6 +119,25 @@
     mount_point = v->mount_point;
   }
 
+  // If we can't acquire the block device for a logical partition, it likely
+  // was never created. In that case we try to create it.
+  if (fs_mgr_is_logical(v) && !fs_mgr_update_logical_partition(v)) {
+    if (did_map_logical_partitions) {
+      LOG(ERROR) << "Failed to find block device for partition";
+      return -1;
+    }
+    std::string super_name = fs_mgr_get_super_partition_name();
+    if (!android::fs_mgr::CreateLogicalPartitions(super_name)) {
+      LOG(ERROR) << "Failed to create logical partitions";
+      return -1;
+    }
+    did_map_logical_partitions = true;
+    if (!fs_mgr_update_logical_partition(v)) {
+      LOG(ERROR) << "Failed to find block device for partition";
+      return -1;
+    }
+  }
+
   const MountedVolume* mv = find_mounted_volume_by_mount_point(mount_point);
   if (mv != nullptr) {
     // Volume is already mounted.
@@ -387,3 +408,7 @@
   }
   return 0;
 }
+
+bool logical_partitions_mapped() {
+  return did_map_logical_partitions;
+}
diff --git a/roots.h b/roots.h
index 46bb77e..702af8d 100644
--- a/roots.h
+++ b/roots.h
@@ -53,4 +53,6 @@
 // mounted (/tmp and /cache) are mounted.  Returns 0 on success.
 int setup_install_mounts();
 
+bool logical_partitions_mapped();
+
 #endif  // RECOVERY_ROOTS_H_
diff --git a/screen_ui.cpp b/screen_ui.cpp
index 2db27d6..ed71888 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -197,11 +197,16 @@
   return offset;
 }
 
-GraphicMenu::GraphicMenu(GRSurface* graphic_headers, const std::vector<GRSurface*>& graphic_items,
+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),
-      graphic_items_(graphic_items) {}
+    : 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()));
@@ -221,7 +226,7 @@
 
 int GraphicMenu::DrawHeader(int x, int y) const {
   draw_funcs_.SetColor(UIElement::HEADER);
-  draw_funcs_.DrawTextIcon(x, y, graphic_headers_);
+  draw_funcs_.DrawTextIcon(x, y, graphic_headers_.get());
   return graphic_headers_->height;
 }
 
@@ -242,7 +247,7 @@
       // Bold white text for the selected item.
       draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
     }
-    draw_funcs_.DrawTextIcon(x, y + offset, item);
+    draw_funcs_.DrawTextIcon(x, y + offset, item.get());
     offset += item->height;
 
     draw_funcs_.SetColor(UIElement::MENU);
@@ -251,8 +256,8 @@
   return offset;
 }
 
-bool GraphicMenu::Validate(size_t max_width, size_t max_height, GRSurface* graphic_headers,
-                           const std::vector<GRSurface*>& graphic_items) {
+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;
@@ -307,7 +312,9 @@
       animation_fps_(
           android::base::GetIntProperty("ro.recovery.ui.animation_fps", kDefaultAnimationFps)),
       density_(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
-      currentIcon(NONE),
+      current_icon_(NONE),
+      current_frame_(0),
+      intro_done_(false),
       progressBarType(EMPTY),
       progressScopeStart(0),
       progressScopeSize(0),
@@ -322,10 +329,6 @@
       show_text_ever(false),
       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_(""),
@@ -340,23 +343,23 @@
   gr_exit();
 }
 
-GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
-  if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
-    return intro_done ? loopFrames[current_frame] : introFrames[current_frame];
+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;
+  return error_icon_.get();
 }
 
-GRSurface* ScreenRecoveryUI::GetCurrentText() const {
-  switch (currentIcon) {
+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();
   }
@@ -391,20 +394,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).
@@ -413,20 +417,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 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);
@@ -437,8 +441,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;
@@ -447,8 +451,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();
@@ -464,19 +468,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);
         }
       }
     }
@@ -518,15 +523,14 @@
   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));
   }
 
   std::lock_guard<std::mutex> lg(updateMutex);
@@ -753,16 +757,16 @@
 
       // 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;
+      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;
+            ++current_frame_;
           }
         } else {
-          current_frame = (current_frame + 1) % loop_frames;
+          current_frame_ = (current_frame_ + 1) % loop_frames_.size();
         }
 
         redraw = true;
@@ -791,18 +795,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) {
@@ -817,9 +826,9 @@
 // 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();
 }
@@ -838,10 +847,6 @@
 // TODO(xunchang) load localized text icons for the menu. (Init for screenRecoveryUI but
 // not wearRecoveryUI).
 bool ScreenRecoveryUI::LoadWipeDataMenuText() {
-  wipe_data_menu_header_text_ = nullptr;
-  factory_data_reset_text_ = nullptr;
-  try_again_text_ = nullptr;
-
   return true;
 }
 
@@ -869,21 +874,20 @@
   // 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();
 
@@ -915,32 +919,34 @@
     }
   }
 
-  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) {
   std::lock_guard<std::mutex> lg(updateMutex);
 
-  currentIcon = icon;
+  current_icon_ = icon;
   update_screen_locked();
 }
 
@@ -972,7 +978,7 @@
   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;
@@ -1115,11 +1121,10 @@
   text_row_ = old_text_row;
 }
 
-std::unique_ptr<Menu> ScreenRecoveryUI::CreateMenu(GRSurface* graphic_header,
-                                                   const std::vector<GRSurface*>& graphic_items,
-                                                   const std::vector<std::string>& text_headers,
-                                                   const std::vector<std::string>& text_items,
-                                                   size_t initial_selection) const {
+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.
@@ -1235,9 +1240,9 @@
 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_, { try_again_text_, factory_data_reset_text_ },
-                 backup_headers, backup_items, 0);
+  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;
   }
diff --git a/screen_ui.h b/screen_ui.h
index 2d0d97d..ff245a2 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -162,32 +162,31 @@
   int char_height_;
 };
 
-// This class uses GRSurfaces* as the menu header and items.
+// 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|.
-  GraphicMenu(GRSurface* graphic_headers, const std::vector<GRSurface*>& graphic_items,
+  // 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 GRSurfaces; and that they can fit in the area
+  // 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, GRSurface* graphic_headers,
-                       const std::vector<GRSurface*>& graphic_items);
+  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:
-  // Pointers to the menu headers and items in graphic icons. This class does not have the ownership
-  // of the these objects.
-  GRSurface* graphic_headers_;
-  std::vector<GRSurface*> graphic_items_;
+  // 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
@@ -243,6 +242,7 @@
 
  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 margin_width_;
@@ -261,8 +261,8 @@
   // 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(GRSurface* graphic_header,
-                                           const std::vector<GRSurface*>& graphic_items,
+  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;
@@ -288,8 +288,8 @@
   virtual void update_screen_locked();
   virtual void update_progress_locked();
 
-  GRSurface* GetCurrentFrame() const;
-  GRSurface* GetCurrentText() const;
+  const GRSurface* GetCurrentFrame() const;
+  const GRSurface* GetCurrentText() const;
 
   void ProgressThreadLoop();
 
@@ -299,8 +299,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;
@@ -324,30 +324,34 @@
   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;
 
-  Icon currentIcon;
-
   // 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> wipe_data_menu_header_text_;
+  std::unique_ptr<GRSurface> try_again_text_;
+  std::unique_ptr<GRSurface> factory_data_reset_text_;
 
-  // Graphs for the wipe data menu
-  GRSurface* wipe_data_menu_header_text_;
-  GRSurface* try_again_text_;
-  GRSurface* factory_data_reset_text_;
+  // 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** introFrames;
-  GRSurface** loopFrames;
-
-  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;
 
@@ -377,13 +381,6 @@
   std::thread progress_thread_;
   std::atomic<bool> progress_thread_stopped_{ false };
 
-  // Number of intro frames and loop frames in the animation.
-  size_t intro_frames;
-  size_t loop_frames;
-
-  size_t current_frame;
-  bool intro_done;
-
   int stage, max_stage;
 
   int char_width_;
diff --git a/tests/Android.bp b/tests/Android.bp
index 2cfc325..5165ccb 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -92,7 +92,7 @@
     "libhidl-gen-utils",
     "libhidlbase",
     "libhidltransport",
-    "libhwbinder",
+    "libhwbinder_noltopgo",
     "libbinderthreadstate",
     "libvndksupport",
     "libtinyxml2",
diff --git a/tests/component/applypatch_modes_test.cpp b/tests/component/applypatch_modes_test.cpp
index ce01f4f..08414b7 100644
--- a/tests/component/applypatch_modes_test.cpp
+++ b/tests/component/applypatch_modes_test.cpp
@@ -23,7 +23,6 @@
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <bsdiff/bsdiff.h>
 #include <gtest/gtest.h>
 #include <openssl/sha.h>
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/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp
index cb4868a..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>
diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp
index 08b4290..27a01cb 100644
--- a/tests/component/install_test.cpp
+++ b/tests/component/install_test.cpp
@@ -20,13 +20,13 @@
 #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>
@@ -36,15 +36,23 @@
 #include "otautil/paths.h"
 #include "private/install.h"
 
-TEST(InstallTest, verify_package_compatibility_no_entry) {
-  TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
+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);
-  // The archive must have something to be opened correctly.
-  ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0));
-  ASSERT_EQ(0, writer.FinishEntry());
+  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;
+  // The archive must have something to be opened correctly.
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
 
   // Doesn't contain compatibility zip entry.
   ZipArchiveHandle zip;
@@ -55,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;
@@ -71,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));
@@ -166,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));
@@ -202,13 +167,8 @@
 
 TEST(InstallTest, SetUpNonAbUpdateCommands) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
   static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
-  ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { UPDATE_BINARY_NAME, "" } }, temp_file.release(), kCompressStored);
 
   ZipArchiveHandle zip;
   ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
@@ -246,13 +206,8 @@
 
 TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
   // The archive must have something to be opened correctly.
-  ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0));
-  ASSERT_EQ(0, writer.FinishEntry());
-  ASSERT_EQ(0, writer.Finish());
-  ASSERT_EQ(0, fclose(zip_file));
+  BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored);
 
   // Missing update binary.
   ZipArchiveHandle zip;
@@ -268,16 +223,8 @@
 
 static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) {
   TemporaryFile temp_file;
-  FILE* zip_file = fdopen(temp_file.release(), "w");
-  ZipWriter writer(zip_file);
-  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", "");
@@ -288,21 +235,27 @@
   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));
+  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));
-  int status_fd = 10;
-  std::string package = "/path/to/update.zip";
-  std::vector<std::string> cmd;
+
+  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]);
@@ -311,7 +264,7 @@
     ASSERT_EQ("--headers=" + properties, cmd[3]);
     ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
   } else {
-    ASSERT_EQ(INSTALL_ERROR, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
+    ASSERT_EQ(INSTALL_ERROR, CheckPackageMetadata(metadata, OtaType::AB));
   }
   CloseArchive(zip);
 }
@@ -323,13 +276,7 @@
 
 TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) {
   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));
+
   std::string device = android::base::GetProperty("ro.product.device", "");
   ASSERT_NE("", device);
   std::string timestamp = android::base::GetProperty("ro.build.date.utc", "");
@@ -339,10 +286,13 @@
           "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));
@@ -381,3 +331,241 @@
   // String with the matching serialno should pass the verification.
   VerifyAbUpdateCommands(long_serialno);
 }
+
+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 (size_t c = 'a'; c <= 'z'; c++) {
+    serial_numbers.emplace_back(c, serialno.size());
+  }
+
+  // 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
index 54329db..d7fdb8f 100644
--- a/tests/component/resources_test.cpp
+++ b/tests/component/resources_test.cpp
@@ -101,7 +101,7 @@
     EXPECT_LT(0, len) << "Locale string should be non-empty.";
     EXPECT_NE(0, row[5]) << "Locale string is missing.";
 
-    ASSERT_GT(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
+    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));
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 2420c27..0a59403 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -24,7 +24,6 @@
 #include <android-base/file.h>
 #include <android-base/properties.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <google/protobuf/repeated_field.h>
 #include <gtest/gtest.h>
 
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index 24c63e7..a0a7b66 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -23,6 +23,7 @@
 #include <algorithm>
 #include <memory>
 #include <string>
+#include <string_view>
 #include <unordered_map>
 #include <vector>
 
@@ -32,7 +33,6 @@
 #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>
@@ -134,9 +134,9 @@
   CloseArchive(handle);
 }
 
-static std::string get_sha1(const std::string& content) {
+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);
 }
 
@@ -187,7 +187,7 @@
 
     // Clear partition updated marker if any.
     std::string updated_marker{ temp_stash_base_.path };
-    updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+    updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED";
     ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
   }
 
@@ -223,14 +223,14 @@
   std::string source_content;
   ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content));
   size_t source_size = source_content.size();
-  std::string source_hash = get_sha1(source_content);
+  std::string source_hash = GetSha1(source_content);
   Partition source(source_file, source_size, source_hash);
 
   std::string target_file = from_testdata_base("recovery.img");
   std::string target_content;
   ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content));
   size_t target_size = target_content.size();
-  std::string target_hash = get_sha1(target_content);
+  std::string target_hash = GetSha1(target_content);
   Partition target(target_file, target_size, target_hash);
 
   // One argument is not valid.
@@ -619,54 +619,100 @@
   RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure);
 }
 
-TEST_F(UpdaterTest, block_image_update_patch_data) {
-  std::string src_content = std::string(4096, 'a') + std::string(4096, 'c');
-  std::string tgt_content = std::string(4096, 'b') + std::string(4096, 'd');
-
+// 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::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",
-    "2",
+    std::to_string(target_blocks),
     "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,
+    "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
   };
 
-  PackageEntries entries{
+  *entries = {
     { "new_data", "" },
     { "patch_data", patch_content },
     { "transfer_list", android::base::Join(transfer_list, '\n') },
   };
+}
 
-  ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
+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_content;
-  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content));
-  ASSERT_EQ(tgt_content, updated_content);
+  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 = get_sha1(src_content);
+  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
@@ -692,7 +738,7 @@
   RunBlockImageUpdate(false, entries, image_file_, "");
 
   // Updater generates the stash name based on the input file name.
-  std::string name_digest = get_sha1(image_file_);
+  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.
@@ -796,9 +842,9 @@
   std::string block1(4096, '1');
   std::string block2(4096, '2');
   std::string block3(4096, '3');
-  std::string block1_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
-  std::string block3_hash = get_sha1(block3);
+  std::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{
@@ -864,8 +910,8 @@
 TEST_F(UpdaterTest, last_command_update_unresumable) {
   std::string block1(4096, '1');
   std::string block2(4096, '2');
-  std::string block1_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
+  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{
@@ -901,9 +947,9 @@
   std::string block1(4096, '1');
   std::string block2(4096, '2');
   std::string block3(4096, '3');
-  std::string block1_hash = get_sha1(block1);
-  std::string block2_hash = get_sha1(block2);
-  std::string block3_hash = get_sha1(block3);
+  std::string block1_hash = GetSha1(block1);
+  std::string block2_hash = GetSha1(block2);
+  std::string block3_hash = GetSha1(block3);
 
   std::vector<std::string> transfer_list_verify{
     // clang-format off
@@ -972,7 +1018,7 @@
 
     // Clear partition updated marker if any.
     std::string updated_marker{ temp_stash_base_.path };
-    updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+    updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED";
     ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
   }
 
@@ -1003,10 +1049,10 @@
   std::string i(4096, 'i');
   std::string zero(4096, '\0');
 
-  std::string a_hash = get_sha1(a);
-  std::string b_hash = get_sha1(b);
-  std::string c_hash = get_sha1(c);
-  std::string e_hash = get_sha1(e);
+  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, "-");
@@ -1027,8 +1073,8 @@
   // patch 1: "b d c" -> "g"
   TemporaryFile patch_file_bdc_g;
   std::string bdc = b + d + c;
-  std::string bdc_hash = get_sha1(bdc);
-  std::string g_hash = get_sha1(g);
+  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));
@@ -1038,9 +1084,9 @@
   // patch 2: "a b c d" -> "d c b"
   TemporaryFile patch_file_abcd_dcb;
   std::string abcd = a + b + c + d;
-  std::string abcd_hash = get_sha1(abcd);
+  std::string abcd_hash = GetSha1(abcd);
   std::string dcb = d + c + b;
-  std::string dcb_hash = get_sha1(dcb);
+  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));
diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp
index d110c37..9fcaa0b 100644
--- a/tests/component/verifier_test.cpp
+++ b/tests/component/verifier_test.cpp
@@ -27,9 +27,11 @@
 #include <android-base/file.h>
 #include <android-base/stringprintf.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.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"
@@ -148,6 +150,35 @@
   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());
@@ -206,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());
     }
   }
 
@@ -221,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));
@@ -298,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));
@@ -324,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/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/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
index 066f981..794f2c1 100644
--- a/tests/unit/applypatch_test.cpp
+++ b/tests/unit/applypatch_test.cpp
@@ -32,7 +32,6 @@
 #include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/stringprintf.h>
-#include <android-base/test_utils.h>
 #include <android-base/unique_fd.h>
 #include <gtest/gtest.h>
 
diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp
index 1ca786c..4dd111a 100644
--- a/tests/unit/dirutil_test.cpp
+++ b/tests/unit/dirutil_test.cpp
@@ -20,7 +20,7 @@
 
 #include <string>
 
-#include <android-base/test_utils.h>
+#include <android-base/file.h>
 #include <gtest/gtest.h>
 
 #include "otautil/dirutil.h"
diff --git a/tests/unit/minui_test.cpp b/tests/unit/minui_test.cpp
index cad6a3d..d68e5e3 100644
--- a/tests/unit/minui_test.cpp
+++ b/tests/unit/minui_test.cpp
@@ -15,8 +15,9 @@
  */
 
 #include <stdint.h>
+#include <stdlib.h>
 
-#include <memory>
+#include <vector>
 
 #include <gtest/gtest.h>
 
@@ -25,8 +26,19 @@
 TEST(GRSurfaceTest, Create_aligned) {
   static constexpr size_t kSurfaceDataAlignment = 8;
   for (size_t data_size = 100; data_size < 128; data_size++) {
-    std::unique_ptr<GRSurface> surface(GRSurface::Create(data_size));
+    auto surface = GRSurface::Create(10, 1, 10, 1, data_size);
     ASSERT_TRUE(surface);
     ASSERT_EQ(0, reinterpret_cast<uintptr_t>(surface->data()) % kSurfaceDataAlignment);
   }
 }
+
+TEST(GRSurfaceTest, Clone) {
+  static constexpr size_t kImageSize = 10 * 50;
+  auto image = GRSurface::Create(50, 10, 50, 1, kImageSize);
+  for (auto i = 0; i < kImageSize; i++) {
+    image->data()[i] = rand() % 128;
+  }
+  auto image_copy = image->Clone();
+  ASSERT_EQ(std::vector(image->data(), image->data() + kImageSize),
+            std::vector(image_copy->data(), image_copy->data() + kImageSize));
+}
diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp
index 8061f3b..72169a0 100644
--- a/tests/unit/parse_install_logs_test.cpp
+++ b/tests/unit/parse_install_logs_test.cpp
@@ -20,7 +20,6 @@
 
 #include <android-base/file.h>
 #include <android-base/strings.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 
 #include "otautil/parse_install_logs.h"
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
index 0014e45..09c4997 100644
--- a/tests/unit/screen_ui_test.cpp
+++ b/tests/unit/screen_ui_test.cpp
@@ -23,10 +23,11 @@
 #include <string>
 #include <vector>
 
+#include <android-base/file.h>
 #include <android-base/logging.h>
 #include <android-base/stringprintf.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
+#include <gtest/gtest_prod.h>
 
 #include "common/test_constants.h"
 #include "device.h"
@@ -69,17 +70,6 @@
   MockDrawFunctions draw_funcs_;
 };
 
-// TODO(xunchang) Create a constructor.
-static GRSurface CreateFakeGRSurface(int width, int height, int row_bytes, int pixel_bytes) {
-  GRSurface fake_surface;
-  fake_surface.width = width;
-  fake_surface.height = height;
-  fake_surface.row_bytes = row_bytes;
-  fake_surface.pixel_bytes = pixel_bytes;
-
-  return fake_surface;
-}
-
 TEST_F(ScreenUITest, StartPhoneMenuSmoke) {
   TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
   ASSERT_FALSE(menu.scrollable());
@@ -241,9 +231,14 @@
 }
 
 TEST_F(ScreenUITest, GraphicMenuSelection) {
-  auto fake_surface = CreateFakeGRSurface(50, 50, 50, 1);
-  std::vector<GRSurface*> items = { &fake_surface, &fake_surface, &fake_surface };
-  GraphicMenu menu(&fake_surface, items, 0, draw_funcs_);
+  auto image = GRSurface::Create(50, 50, 50, 1, 50 * 50);
+  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());
 
@@ -263,18 +258,23 @@
 }
 
 TEST_F(ScreenUITest, GraphicMenuValidate) {
-  auto fake_surface = CreateFakeGRSurface(50, 50, 50, 1);
-  std::vector<GRSurface*> items = { &fake_surface, &fake_surface, &fake_surface };
+  auto image = GRSurface::Create(50, 50, 50, 1, 50 * 50);
+  auto header = image->Clone();
+  std::vector<const GRSurface*> items = {
+    image.get(),
+    image.get(),
+    image.get(),
+  };
 
-  ASSERT_TRUE(GraphicMenu::Validate(200, 200, &fake_surface, items));
+  ASSERT_TRUE(GraphicMenu::Validate(200, 200, header.get(), items));
 
   // Menu exceeds the horizontal boundary.
-  auto wide_surface = CreateFakeGRSurface(300, 50, 300, 1);
-  ASSERT_FALSE(GraphicMenu::Validate(299, 200, &wide_surface, items));
+  auto wide_surface = GRSurface::Create(300, 50, 300, 1, 300 * 50);
+  ASSERT_FALSE(GraphicMenu::Validate(299, 200, wide_surface.get(), items));
 
   // Menu exceeds the vertical boundary.
-  items.push_back(&fake_surface);
-  ASSERT_FALSE(GraphicMenu::Validate(200, 249, &fake_surface, items));
+  items.emplace_back(image.get());
+  ASSERT_FALSE(GraphicMenu::Validate(200, 249, header.get(), items));
 }
 
 static constexpr int kMagicAction = 101;
@@ -307,24 +307,13 @@
 
   int KeyHandler(int key, bool visible) const;
 
-  // The following functions expose the protected members for test purpose.
-  void RunLoadAnimation() {
-    LoadAnimation();
-  }
-
-  size_t GetLoopFrames() const {
-    return loop_frames;
-  }
-
-  size_t GetIntroFrames() const {
-    return intro_frames;
-  }
-
-  bool GetRtlLocale() const {
-    return rtl_locale_;
-  }
-
  private:
+  FRIEND_TEST(ScreenRecoveryUITest, Init);
+  FRIEND_TEST(ScreenRecoveryUITest, RtlLocale);
+  FRIEND_TEST(ScreenRecoveryUITest, RtlLocaleWithSuffix);
+  FRIEND_TEST(ScreenRecoveryUITest, LoadAnimation);
+  FRIEND_TEST(ScreenRecoveryUITest, LoadAnimation_MissingAnimation);
+
   std::vector<KeyCode> key_buffer_;
   size_t key_buffer_index_;
 };
@@ -388,7 +377,7 @@
 
   ASSERT_TRUE(ui_->Init(kTestLocale));
   ASSERT_EQ(kTestLocale, ui_->GetLocale());
-  ASSERT_FALSE(ui_->GetRtlLocale());
+  ASSERT_FALSE(ui_->rtl_locale_);
   ASSERT_FALSE(ui_->IsTextVisible());
   ASSERT_FALSE(ui_->WasTextEverVisible());
 }
@@ -416,14 +405,14 @@
   RETURN_IF_NO_GRAPHICS;
 
   ASSERT_TRUE(ui_->Init(kTestRtlLocale));
-  ASSERT_TRUE(ui_->GetRtlLocale());
+  ASSERT_TRUE(ui_->rtl_locale_);
 }
 
 TEST_F(ScreenRecoveryUITest, RtlLocaleWithSuffix) {
   RETURN_IF_NO_GRAPHICS;
 
   ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix));
-  ASSERT_TRUE(ui_->GetRtlLocale());
+  ASSERT_TRUE(ui_->rtl_locale_);
 }
 
 TEST_F(ScreenRecoveryUITest, ShowMenu) {
@@ -548,10 +537,10 @@
   }
   Paths::Get().set_resource_dir(resource_dir.path);
 
-  ui_->RunLoadAnimation();
+  ui_->LoadAnimation();
 
-  ASSERT_EQ(2u, ui_->GetIntroFrames());
-  ASSERT_EQ(3u, ui_->GetLoopFrames());
+  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()));
@@ -567,7 +556,7 @@
   Paths::Get().set_resource_dir("/proc/self");
 
   ::testing::FLAGS_gtest_death_test_style = "threadsafe";
-  ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), "");
+  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 de8ff70..77625db 100644
--- a/tests/unit/sysutil_test.cpp
+++ b/tests/unit/sysutil_test.cpp
@@ -17,7 +17,6 @@
 #include <string>
 
 #include <android-base/file.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 
 #include "otautil/sysutil.h"
diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp
index 47f33d9..dfe617e 100644
--- a/tests/unit/zip_test.cpp
+++ b/tests/unit/zip_test.cpp
@@ -21,7 +21,6 @@
 #include <vector>
 
 #include <android-base/file.h>
-#include <android-base/test_utils.h>
 #include <gtest/gtest.h>
 #include <ziparchive/zip_archive.h>
 
diff --git a/tools/image_generator/Android.bp b/tools/image_generator/Android.bp
index 3f718fe..ce6e277 100644
--- a/tools/image_generator/Android.bp
+++ b/tools/image_generator/Android.bp
@@ -17,7 +17,11 @@
 
     manifest: "ImageGenerator.mf",
 
+    static_libs: [
+        "commons-cli-1.2",
+    ],
+
     srcs: [
         "ImageGenerator.java",
     ],
-}
\ No newline at end of file
+}
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
index f226216..8730945 100644
--- a/tools/image_generator/ImageGenerator.java
+++ b/tools/image_generator/ImageGenerator.java
@@ -16,6 +16,16 @@
 
 package com.android.recovery.tools;
 
+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;
@@ -26,369 +36,586 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
-import java.util.TreeMap;
+import java.util.Set;
 import java.util.StringTokenizer;
+import java.util.TreeMap;
 
 import javax.imageio.ImageIO;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
-import org.w3c.dom.Document;
-import org.w3c.dom.Node;
-import org.w3c.dom.NodeList;
-
-/**
- * Command line tool to generate the localized image for recovery mode.
- */
+/** 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;
+    // 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 float DEFAULT_FONT_SIZE = 40;
 
-  // This is the canvas we used to draw texts.
-  private BufferedImage mBufferedImage;
+    // This is the canvas we used to draw texts.
+    private BufferedImage mBufferedImage;
 
-  // The width in pixels of our image. Once set, its value won't change.
-  private final int mImageWidth;
+    // 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 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 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 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 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;
+    // The directory that contains all the needed font files (e.g. ttf, otf, ttc files).
+    private final String mFontDirPath;
 
-  // 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");
-  }};
+    // Align the text in the center of the image.
+    private final boolean mCenterAlignment;
 
-  /**
-   * Exception to indicate the failure to find the translated text strings.
-   */
-  public static class LocalizedStringNotFoundException extends Exception {
-    public LocalizedStringNotFoundException(String message) {
-      super(message);
+    // 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
+                }
+            };
+
+    // Languages that breaks on arbitrary characters.
+    // TODO(xunchang) switch to icu library if possible. For example, for Thai and Khmer, there is
+    // no space between words; and word breaking is based on grammatical analysis and on word
+    // matching in dictionaries.
+    private static final Set<String> LOGOGRAM_LANGUAGE =
+            new HashSet<String>() {
+                {
+                    add("ja"); // Japanese
+                    add("km"); // Khmer
+                    add("ko"); // Korean
+                    add("lo"); // Lao
+                    add("th"); // Thai
+                    add("zh"); // Chinese
+                }
+            };
+
+    /** 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);
+        }
     }
 
-    public LocalizedStringNotFoundException(String message, Throwable cause) {
-      super(message, cause);
-    }
-  }
+    /** 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;
 
-  /**
-   * Initailizes the fields of the image image.
-   */
-  public ImageGenerator(int imageWidth, String textName, float fontSize, String fontDirPath) {
-    mImageWidth = imageWidth;
-    mImageHeight = INITIAL_HEIGHT;
-    mVerticalOffset = 0;
+        // Initialize the canvas with the default height.
+        mBufferedImage = new BufferedImage(mImageWidth, mImageHeight, BufferedImage.TYPE_BYTE_GRAY);
 
-    // Initialize the canvas with the default height.
-    mBufferedImage = new BufferedImage(mImageWidth, mImageHeight, BufferedImage.TYPE_BYTE_GRAY);
+        mTextName = textName;
+        mFontSize = fontSize;
+        mFontDirPath = fontDirPath;
 
-    mTextName = textName;
-    mFontSize = fontSize;
-    mFontDirPath = fontDirPath;
-  }
-
-  /**
-   * 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();
-      }
+        mCenterAlignment = centerAlignment;
     }
 
-    throw new LocalizedStringNotFoundException(textName + " not found in "
-        + resourceFile.getName());
-  }
+    /**
+     * 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();
 
-  /**
-   * 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));
-    }
+        Document doc = db.parse(resourceFile);
+        doc.getDocumentElement().normalize();
 
-    throw new IOException("Unrecognized locale string " + localeString);
-  }
+        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();
+            }
+        }
 
-  /**
-   * 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
-   *
-   * @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) throws
-      IOException, LocalizedStringNotFoundException {
-    File resourceDir = new File(resourcePath);
-    if (!resourceDir.isDirectory()) {
-      throw new LocalizedStringNotFoundException(resourcePath + " is not a directory.");
-    }
-
-    Map<Locale, String> result =
-        new TreeMap<Locale, String>(Comparator.comparing(Locale::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) {
-      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));
+                textName + " not found in " + resourceFile.getName());
     }
 
-    return result;
-  }
+    /** 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));
+        }
 
-  /**
-   * 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 {
-    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()) {
-        return Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize);
-      }
+        throw new IOException("Unrecognized locale string " + localeString);
     }
 
-    throw new IOException("Can not find the font file " + fontName + " for language " + language);
-  }
+    /**
+     * 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
+     * @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)
+            throws IOException, LocalizedStringNotFoundException {
+        File resourceDir = new File(resourcePath);
+        if (!resourceDir.isDirectory()) {
+            throw new LocalizedStringNotFoundException(resourcePath + " is not a directory.");
+        }
 
-  /**
-   * Separates the text string by spaces and wraps it by words.
-  **/
-  private List<String> wrapTextByWords(String text, FontMetrics metrics) {
-    List<String> wrappedText = new ArrayList<>();
-    StringTokenizer st = new StringTokenizer(text, " \n");
+        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());
+                        });
 
-    StringBuilder line = new StringBuilder();
-    while (st.hasMoreTokens()) {
-      String token = st.nextToken();
-      if (metrics.stringWidth(line + token + " ") > mImageWidth) {
+        // 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) {
+            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 {
+        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()) {
+                return Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize);
+            }
+        }
+
+        throw new IOException(
+                "Can not find the font file " + fontName + " for language " + language);
+    }
+
+    /** Separates the text string by spaces and wraps it by words. */
+    private List<String> wrapTextByWords(String text, FontMetrics metrics) {
+        List<String> wrappedText = new ArrayList<>();
+        StringTokenizer st = new StringTokenizer(text, " \n");
+
+        StringBuilder line = new StringBuilder();
+        while (st.hasMoreTokens()) {
+            String token = st.nextToken();
+            if (metrics.stringWidth(line + token + " ") > mImageWidth) {
+                wrappedText.add(line.toString());
+                line = new StringBuilder();
+            }
+            line.append(token).append(" ");
+        }
         wrappedText.add(line.toString());
-        line = new StringBuilder();
-      }
-      line.append(token).append(" ");
-    }
-    wrappedText.add(line.toString());
 
-    return wrappedText;
-  }
-
-  /**
-   * 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 list of strings with their width smaller than mImageWidth pixels
-   */
-  private List<String> wrapText(String text, FontMetrics metrics) {
-    // TODO(xunchang) handle other cases of text wrapping
-    // 1. RTL languages: "ar"(Arabic), "fa"(Persian), "he"(Hebrew), "iw"(Hebrew), "ur"(Urdu)
-    // 2. Language uses characters: CJK, "lo"(lao), "km"(khmer)
-
-    return wrapTextByWords(text, metrics);
-  }
-
-  /**
-   * 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) 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()));
-
-    System.out.println("Drawing text for locale " + locale + " text " + text);
-
-    FontMetrics fontMetrics = graphics.getFontMetrics();
-    List<String> wrappedText = wrapTextByWords(text, fontMetrics);
-    for (String line : wrappedText) {
-      int lineHeight = fontMetrics.getHeight();
-      // Doubles the height of the image if we are short of space.
-      if (mVerticalOffset + lineHeight >= mImageHeight) {
-        resizeHeight(mImageHeight * 2);
-      }
-
-      // Draws the text at mVerticalOffset and increments the offset with line space.
-      int baseLine = mVerticalOffset + lineHeight - fontMetrics.getDescent();
-      graphics.drawString(line, 0, baseLine);
-      mVerticalOffset += lineHeight;
-    }
-  }
-
-  /**
-   * Redraws the image with the new height.
-   *
-   * @param height the new height of the image in pixels.
-   */
-  private void resizeHeight(int height) {
-    BufferedImage resizedImage =
-        new BufferedImage(mImageWidth, height, BufferedImage.TYPE_BYTE_GRAY);
-    Graphics2D graphic = resizedImage.createGraphics();
-    graphic.drawImage(mBufferedImage, 0, 0, null);
-    graphic.dispose();
-
-    mBufferedImage = resizedImage;
-    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 {
-    for (Locale locale : localizedTextMap.keySet()) {
-      // TODO(xunchang) reprocess the locales for the same language and make the last locale the
-      // catch-all type. e.g. "zh-CN, zh-HK, zh-TW" will become "zh-CN, zh-HK, zh"
-      // Or maybe we don't need to support these variants?
-      drawText(localizedTextMap.get(locale), locale);
+        return wrappedText;
     }
 
-    // TODO(xunchang) adjust the width to save some space if all texts are smaller than imageWidth.
-    resizeHeight(mVerticalOffset);
-    ImageIO.write(mBufferedImage, "png", new File(outputPath));
-  }
+    /** One character is a word for CJK. */
+    private List<String> wrapTextByCharacters(String text, FontMetrics metrics) {
+        List<String> wrappedText = new ArrayList<>();
 
-  public static void printUsage() {
-    System.out.println("Usage: java -jar path_to_jar imageWidth textName fontDirectory"
-        + " resourceDirectory outputFilename");
-  }
+        StringBuilder line = new StringBuilder();
+        for (char token : text.toCharArray()) {
+            if (metrics.stringWidth(line + Character.toString(token)) > mImageWidth) {
+                wrappedText.add(line.toString());
+                line = new StringBuilder();
+            }
+            line.append(token);
+        }
+        wrappedText.add(line.toString());
 
-  public static void main(String[] args) throws NumberFormatException, IOException,
-      FontFormatException, LocalizedStringNotFoundException {
-    if (args.length != 5) {
-      printUsage();
-      System.err.println("We expect 5 arguments, get " + args.length);
-      System.exit(1);
+        return wrappedText;
     }
 
-    // TODO(xunchang) switch to commandline parser
-    int imageWidth = Integer.parseUnsignedInt(args[0]);
+    /**
+     * 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 list of strings with their width smaller than mImageWidth pixels
+     */
+    private List<String> wrapText(String text, FontMetrics metrics, String language) {
+        if (LOGOGRAM_LANGUAGE.contains(language)) {
+            return wrapTextByCharacters(text, metrics);
+        }
 
-    ImageGenerator imageGenerator =
-        new ImageGenerator(imageWidth, args[1], DEFAULT_FONT_SIZE, args[2]);
+        return wrapTextByWords(text, metrics);
+    }
 
-    Map<Locale, String> localizedStringMap =
-        imageGenerator.readLocalizedStringFromXmls(args[3]);
-    imageGenerator.generateImage(localizedStringMap, args[4]);
-  }
+    /**
+     * 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();
+        List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+
+        int textWidth = 0;
+        for (String line : wrappedText) {
+            textWidth = Math.max(textWidth, fontMetrics.stringWidth(line));
+        }
+
+        // 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 {
+        System.out.println("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
+
+        Graphics2D graphics = createGraphics(locale);
+        FontMetrics fontMetrics = graphics.getFontMetrics();
+        List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+
+        // 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 (String line : wrappedText) {
+            int lineHeight = fontMetrics.getHeight();
+            // Doubles the height of the image if we are short of space.
+            if (mVerticalOffset + lineHeight >= mImageHeight) {
+                resize(mImageWidth, mImageHeight * 2);
+            }
+
+            // 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 - fontMetrics.stringWidth(line)) / 2
+                            : RTL_LANGUAGE.contains(languageTag)
+                                    ? mImageWidth - fontMetrics.stringWidth(line)
+                                    : 0;
+
+            graphics.drawString(line, 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 {
+        Map<String, Integer> languageCount = new TreeMap<>();
+        int textWidth = 0;
+        for (Locale locale : localizedTextMap.keySet()) {
+            String language = locale.getLanguage();
+            languageCount.put(language, languageCount.getOrDefault(language, 0) + 1);
+            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()) {
+            Integer count = languageCount.get(locale.getLanguage());
+            // Recovery expects en-US instead of en_US.
+            String languageTag = locale.toLanguageTag();
+            if (count == 1) {
+                // Make the last country variant for a given language be the catch-all for that
+                // language.
+                languageTag = locale.getLanguage();
+            } else {
+                languageCount.put(locale.getLanguage(), count - 1);
+            }
+
+            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());
+
+        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"));
+
+        ImageGenerator imageGenerator =
+                new ImageGenerator(
+                        imageWidth,
+                        cmd.getOptionValue("text_name"),
+                        DEFAULT_FONT_SIZE,
+                        cmd.getOptionValue("font_dir"),
+                        cmd.hasOption("center_alignment"));
+
+        Map<Locale, String> localizedStringMap =
+                imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"));
+        imageGenerator.generateImage(localizedStringMap, cmd.getOptionValue("output_file"));
+    }
 }
-
diff --git a/tools/image_generator/README.md b/tools/image_generator/README.md
index 22e32f6..5d70354 100644
--- a/tools/image_generator/README.md
+++ b/tools/image_generator/README.md
@@ -6,7 +6,8 @@
 emulators with different dpi.
 
 # Usage:
-  `java -jar path_to_jar imageWidth textName fontDirectory resourceDirectory outputFilename`
+  `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
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/updater/blockimg.cpp b/updater/blockimg.cpp
index 47849a1..c4c0909 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -1399,7 +1399,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() << " ["
diff --git a/verifier.cpp b/verifier.cpp
index 2101dcb..44bd4e1 100644
--- a/verifier.cpp
+++ b/verifier.cpp
@@ -308,144 +308,6 @@
   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);
-  }
-};
-
-std::unique_ptr<EC_KEY, ECKEYDeleter> parse_ec_key(FILE* file) {
-    uint32_t key_len_bytes = 0;
-    if (fscanf(file, " %i", &key_len_bytes) != 1) {
-        return nullptr;
-    }
-
-    std::unique_ptr<EC_GROUP, void (*)(EC_GROUP*)> group(
-        EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1), EC_GROUP_free);
-    if (!group) {
-        return nullptr;
-    }
-
-    // Verify that |key_len| matches the group order.
-    if (key_len_bytes != BN_num_bytes(EC_GROUP_get0_order(group.get()))) {
-        return nullptr;
-    }
-
-    // Read the public key coordinates. Note that the byte order in the file is
-    // little-endian, so we convert to big-endian here.
-    std::unique_ptr<uint8_t[]> bytes(new uint8_t[key_len_bytes]);
-    std::unique_ptr<BIGNUM, BNDeleter> point[2];
-    for (int i = 0; i < 2; ++i) {
-        unsigned int byte = 0;
-        if (fscanf(file, " , { %u", &byte) != 1) {
-            return nullptr;
-        }
-        bytes[key_len_bytes - 1] = byte;
-
-        for (size_t i = 1; i < key_len_bytes; ++i) {
-            if (fscanf(file, " , %u", &byte) != 1) {
-                return nullptr;
-            }
-            bytes[key_len_bytes - i - 1] = byte;
-        }
-
-        point[i].reset(BN_bin2bn(bytes.get(), key_len_bytes, nullptr));
-        if (!point[i]) {
-            return nullptr;
-        }
-
-        if (fscanf(file, " }") != 0) {
-            return nullptr;
-        }
-    }
-
-    if (fscanf(file, " } ") != 0) {
-        return nullptr;
-    }
-
-    // Create and initialize the key.
-    std::unique_ptr<EC_KEY, ECKEYDeleter> key(EC_KEY_new());
-    if (!key || !EC_KEY_set_group(key.get(), group.get()) ||
-        !EC_KEY_set_public_key_affine_coordinates(key.get(), point[0].get(),
-                                                  point[1].get())) {
-        return nullptr;
-    }
-
-    return key;
-}
-
 static std::vector<Certificate> IterateZipEntriesAndSearchForKeys(const ZipArchiveHandle& handle) {
   void* cookie;
   ZipString suffix("x509.pem");
@@ -500,6 +362,48 @@
   return result;
 }
 
+bool CheckRSAKey(const std::unique_ptr<RSA, RSADeleter>& rsa) {
+  if (!rsa) {
+    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);
@@ -538,22 +442,20 @@
   }
 
   int key_type = EVP_PKEY_id(public_key.get());
-  // TODO(xunchang) check the rsa key has exponent 3 or 65537 with RSA_get0_key; and ec key is
-  // 256 bits.
   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) {
-      LOG(ERROR) << "Failed to get the rsa key info from public key";
+    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) {
-      LOG(ERROR) << "Failed to get the ec key info from the public key";
+    if (!cert->ec || !CheckECKey(cert->ec)) {
+      LOG(ERROR) << "Failed to validate the ec key info from the public key";
       return false;
     }
   } else {
@@ -563,114 +465,3 @@
 
   return true;
 }
-
-// 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;
-    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;
-    }
-  }
-  return true;
-}
diff --git a/verifier.h b/verifier.h
index b7924c7..df9a4b6 100644
--- a/verifier.h
+++ b/verifier.h
@@ -70,7 +70,11 @@
 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.
diff --git a/wear_ui.cpp b/wear_ui.cpp
index 0611f94..6da84c9 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -51,8 +51,8 @@
   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;
@@ -60,7 +60,7 @@
     gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
 
     // Draw recovery text on screen above progress bar.
-    GRSurface* text = GetCurrentText();
+    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);