Merge "uncrypt: fix ioctl direction for F2FS_IOC_GET_PIN_FILE"
diff --git a/Android.bp b/Android.bp
index afab76c..76e6985 100644
--- a/Android.bp
+++ b/Android.bp
@@ -256,6 +256,7 @@
     shared_libs: [
         "libbase",
         "liblog",
+        "libmetricslogger",
     ],
 
     static_libs: [
diff --git a/Android.mk b/Android.mk
index 80d107d..7be1230 100644
--- a/Android.mk
+++ b/Android.mk
@@ -71,10 +71,13 @@
 endif
 endif
 
+# On A/B devices recovery-persist reads the recovery related file from the persist storage and
+# copies them into /data/misc/recovery. Then, for both A/B and non-A/B devices, recovery-persist
+# parses the last_install file and reports the embedded update metrics. Also, the last_install file
+# will be deteleted after the report.
+LOCAL_REQUIRED_MODULES += recovery-persist
 ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),)
-LOCAL_REQUIRED_MODULES += \
-    recovery-persist \
-    recovery-refresh
+LOCAL_REQUIRED_MODULES += recovery-refresh
 endif
 
 include $(BUILD_PHONY_PACKAGE)
diff --git a/fsck_unshare_blocks.cpp b/fsck_unshare_blocks.cpp
index 2e6b5b8..684958e 100644
--- a/fsck_unshare_blocks.cpp
+++ b/fsck_unshare_blocks.cpp
@@ -40,6 +40,7 @@
 
 static constexpr const char* SYSTEM_E2FSCK_BIN = "/system/bin/e2fsck_static";
 static constexpr const char* TMP_E2FSCK_BIN = "/tmp/e2fsck.bin";
+static constexpr const char* SYSTEM_ROOT = "/system";
 
 static bool copy_file(const char* source, const char* dest) {
   android::base::unique_fd source_fd(open(source, O_RDONLY));
@@ -121,12 +122,12 @@
 
   // Temporarily mount system so we can copy e2fsck_static.
   bool mounted = false;
-  if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
+  if (volume_for_mount_point(SYSTEM_ROOT) == nullptr) {
     mounted = ensure_path_mounted_at("/", "/mnt/system") != -1;
     partitions.push_back("/");
   } else {
-    mounted = ensure_path_mounted_at("/system", "/mnt/system") != -1;
-    partitions.push_back("/system");
+    mounted = ensure_path_mounted_at(SYSTEM_ROOT, "/mnt/system") != -1;
+    partitions.push_back(SYSTEM_ROOT);
   }
   if (!mounted) {
     LOG(ERROR) << "Failed to mount system image.";
diff --git a/install.h b/install.h
index 0f6670a..1d3d0cd 100644
--- a/install.h
+++ b/install.h
@@ -23,8 +23,15 @@
 
 #include <ziparchive/zip_archive.h>
 
-enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED,
-        INSTALL_RETRY };
+enum InstallResult {
+  INSTALL_SUCCESS,
+  INSTALL_ERROR,
+  INSTALL_CORRUPT,
+  INSTALL_NONE,
+  INSTALL_SKIPPED,
+  INSTALL_RETRY,
+  INSTALL_KEY_INTERRUPTED
+};
 
 // Installs the given update package. If INSTALL_SUCCESS is returned and *wipe_cache is true on
 // exit, caller should wipe the cache partition.
diff --git a/logging.cpp b/logging.cpp
index d5af72a..50642a2 100644
--- a/logging.cpp
+++ b/logging.cpp
@@ -221,6 +221,7 @@
   chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
   chmod(LAST_LOG_FILE, 0640);
   chmod(LAST_INSTALL_FILE, 0644);
+  chown(LAST_INSTALL_FILE, AID_SYSTEM, AID_SYSTEM);
   sync();
 }
 
diff --git a/minadbd/Android.bp b/minadbd/Android.bp
index 00244ee..370232b 100644
--- a/minadbd/Android.bp
+++ b/minadbd/Android.bp
@@ -52,6 +52,7 @@
 
 cc_test {
     name: "minadbd_test",
+    isolated: true,
 
     defaults: [
         "minadbd_defaults",
@@ -64,7 +65,6 @@
     static_libs: [
         "libminadbd_services",
         "libadbd",
-        "libBionicGtestMain",
     ],
 
     shared_libs: [
diff --git a/minadbd/minadbd_services.cpp b/minadbd/minadbd_services.cpp
index ab1939e..e9c51da 100644
--- a/minadbd/minadbd_services.cpp
+++ b/minadbd/minadbd_services.cpp
@@ -33,19 +33,20 @@
 #include "sysdeps.h"
 
 static void sideload_host_service(unique_fd sfd, const std::string& args) {
-    int file_size;
-    int block_size;
-    if (sscanf(args.c_str(), "%d:%d", &file_size, &block_size) != 2) {
-        printf("bad sideload-host arguments: %s\n", args.c_str());
-        exit(1);
-    }
+  int64_t file_size;
+  int block_size;
+  if ((sscanf(args.c_str(), "%" SCNd64 ":%d", &file_size, &block_size) != 2) || file_size <= 0 ||
+      block_size <= 0) {
+    printf("bad sideload-host arguments: %s\n", args.c_str());
+    exit(1);
+  }
 
-    printf("sideload-host file size %d block size %d\n", file_size, block_size);
+  printf("sideload-host file size %" PRId64 " block size %d\n", file_size, block_size);
 
-    int result = run_adb_fuse(sfd, file_size, block_size);
+  int result = run_adb_fuse(sfd, file_size, block_size);
 
-    printf("sideload_host finished\n");
-    exit(result == 0 ? 0 : 1);
+  printf("sideload_host finished\n");
+  exit(result == 0 ? 0 : 1);
 }
 
 unique_fd daemon_service_to_fd(const char* name, atransport* /* transport */) {
diff --git a/otautil/Android.bp b/otautil/Android.bp
index 56c7c9e..41018dd 100644
--- a/otautil/Android.bp
+++ b/otautil/Android.bp
@@ -41,6 +41,7 @@
             srcs: [
                 "dirutil.cpp",
                 "mounts.cpp",
+                "parse_install_logs.cpp",
                 "sysutil.cpp",
                 "thermalutil.cpp",
             ],
diff --git a/otautil/include/otautil/parse_install_logs.h b/otautil/include/otautil/parse_install_logs.h
new file mode 100644
index 0000000..135d29c
--- /dev/null
+++ b/otautil/include/otautil/parse_install_logs.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+constexpr const char* LAST_INSTALL_FILE = "/data/misc/recovery/last_install";
+constexpr const char* LAST_INSTALL_FILE_IN_CACHE = "/cache/recovery/last_install";
+
+// Parses the metrics of update applied under recovery mode in |lines|, and returns a map with
+// "name: value".
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines);
+// Parses the sideload history and update metrics in the last_install file. Returns a map with
+// entries as "metrics_name: value". If no such file exists, returns an empty map.
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name);
diff --git a/otautil/parse_install_logs.cpp b/otautil/parse_install_logs.cpp
new file mode 100644
index 0000000..13a7299
--- /dev/null
+++ b/otautil/parse_install_logs.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "otautil/parse_install_logs.h"
+
+#include <unistd.h>
+
+#include <optional>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+
+constexpr const char* OTA_SIDELOAD_METRICS = "ota_sideload";
+
+// Here is an example of lines in last_install:
+// ...
+// time_total: 101
+// bytes_written_vendor: 51074
+// bytes_stashed_vendor: 200
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines) {
+  constexpr unsigned int kMiB = 1024 * 1024;
+  std::optional<int64_t> bytes_written_in_mib;
+  std::optional<int64_t> bytes_stashed_in_mib;
+  std::map<std::string, int64_t> metrics;
+  for (const auto& line : lines) {
+    size_t num_index = line.find(':');
+    if (num_index == std::string::npos) {
+      LOG(WARNING) << "Skip parsing " << line;
+      continue;
+    }
+
+    std::string num_string = android::base::Trim(line.substr(num_index + 1));
+    int64_t parsed_num;
+    if (!android::base::ParseInt(num_string, &parsed_num)) {
+      LOG(ERROR) << "Failed to parse numbers in " << line;
+      continue;
+    }
+
+    if (android::base::StartsWith(line, "bytes_written")) {
+      bytes_written_in_mib = bytes_written_in_mib.value_or(0) + parsed_num / kMiB;
+    } else if (android::base::StartsWith(line, "bytes_stashed")) {
+      bytes_stashed_in_mib = bytes_stashed_in_mib.value_or(0) + parsed_num / kMiB;
+    } else if (android::base::StartsWith(line, "time")) {
+      metrics.emplace("ota_time_total", parsed_num);
+    } else if (android::base::StartsWith(line, "uncrypt_time")) {
+      metrics.emplace("ota_uncrypt_time", parsed_num);
+    } else if (android::base::StartsWith(line, "source_build")) {
+      metrics.emplace("ota_source_version", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_start")) {
+      metrics.emplace("ota_temperature_start", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_end")) {
+      metrics.emplace("ota_temperature_end", parsed_num);
+    } else if (android::base::StartsWith(line, "temperature_max")) {
+      metrics.emplace("ota_temperature_max", parsed_num);
+    } else if (android::base::StartsWith(line, "error")) {
+      metrics.emplace("ota_non_ab_error_code", parsed_num);
+    } else if (android::base::StartsWith(line, "cause")) {
+      metrics.emplace("ota_non_ab_cause_code", parsed_num);
+    }
+  }
+
+  if (bytes_written_in_mib) {
+    metrics.emplace("ota_written_in_MiBs", bytes_written_in_mib.value());
+  }
+  if (bytes_stashed_in_mib) {
+    metrics.emplace("ota_stashed_in_MiBs", bytes_stashed_in_mib.value());
+  }
+
+  return metrics;
+}
+
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name) {
+  if (access(file_name.c_str(), F_OK) != 0) {
+    return {};
+  }
+
+  std::string content;
+  if (!android::base::ReadFileToString(file_name, &content)) {
+    PLOG(ERROR) << "Failed to read " << file_name;
+    return {};
+  }
+
+  if (content.empty()) {
+    LOG(INFO) << "Empty last_install file";
+    return {};
+  }
+
+  std::vector<std::string> lines = android::base::Split(content, "\n");
+  auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+  // LAST_INSTALL starts with "/sideload/package.zip" after a sideload.
+  if (android::base::Trim(lines[0]) == "/sideload/package.zip") {
+    int type = (android::base::GetProperty("ro.build.type", "") == "user") ? 1 : 0;
+    metrics.emplace(OTA_SIDELOAD_METRICS, type);
+  }
+
+  return metrics;
+}
diff --git a/recovery-persist.cpp b/recovery-persist.cpp
index d3ade62..ebb42d2 100644
--- a/recovery-persist.cpp
+++ b/recovery-persist.cpp
@@ -35,19 +35,22 @@
 #include <string.h>
 #include <unistd.h>
 
+#include <limits>
 #include <string>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
+#include <metricslogger/metrics_logger.h>
 #include <private/android_logger.h> /* private pmsg functions */
 
 #include "logging.h"
+#include "otautil/parse_install_logs.h"
 
-static const char *LAST_LOG_FILE = "/data/misc/recovery/last_log";
-static const char *LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
-static const char *LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
-static const char *LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
-static const char *ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
+constexpr const char* LAST_LOG_FILE = "/data/misc/recovery/last_log";
+constexpr const char* LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
+constexpr const char* LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
+constexpr const char* LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
+constexpr const char* ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
 
 // close a file, log an error if the error indicator is set
 static void check_and_fclose(FILE *fp, const char *name) {
@@ -109,6 +112,20 @@
     return android::base::WriteStringToFile(buffer, destination.c_str());
 }
 
+// Parses the LAST_INSTALL file and reports the update metrics saved under recovery mode.
+static void report_metrics_from_last_install(const std::string& file_name) {
+  auto metrics = ParseLastInstall(file_name);
+  // TODO(xunchang) report the installation result.
+  for (const auto& [event, value] : metrics) {
+    if (value > std::numeric_limits<int>::max()) {
+      LOG(WARNING) << event << " (" << value << ") exceeds integer max.";
+    } else {
+      LOG(INFO) << "Uploading " << value << " to " << event;
+      android::metricslogger::LogHistogram(event, value);
+    }
+  }
+}
+
 int main(int argc, char **argv) {
 
     /* Is /cache a mount?, we have been delivered where we are not wanted */
@@ -138,14 +155,18 @@
     }
 
     if (has_cache) {
-        /*
-         * TBD: Future location to move content from
-         * /cache/recovery to /data/misc/recovery/
-         */
-        /* if --force-persist flag, then transfer pmsg data anyways */
-        if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
-            return 0;
-        }
+      // Collects and reports the non-a/b update metrics from last_install; and removes the file
+      // to avoid duplicate report.
+      report_metrics_from_last_install(LAST_INSTALL_FILE_IN_CACHE);
+      if (unlink(LAST_INSTALL_FILE_IN_CACHE) == -1) {
+        PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE_IN_CACHE;
+      }
+
+      // TBD: Future location to move content from /cache/recovery to /data/misc/recovery/
+      // if --force-persist flag, then transfer pmsg data anyways
+      if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
+        return 0;
+      }
     }
 
     /* Is there something in pmsg? */
@@ -157,6 +178,15 @@
     __android_log_pmsg_file_read(
         LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", logsave, NULL);
 
+    // For those device without /cache, the last_install file has been copied to
+    // /data/misc/recovery from pmsg. Looks for the sideload history only.
+    if (!has_cache) {
+      report_metrics_from_last_install(LAST_INSTALL_FILE);
+      if (unlink(LAST_INSTALL_FILE) == -1) {
+        PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE;
+      }
+    }
+
     /* Is there a last console log too? */
     if (rotated) {
         if (!access(LAST_CONSOLE_FILE, R_OK)) {
diff --git a/recovery.cpp b/recovery.cpp
index 5934b61..3ea282f 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -78,6 +78,7 @@
 static constexpr const char* DATA_ROOT = "/data";
 static constexpr const char* METADATA_ROOT = "/metadata";
 static constexpr const char* SDCARD_ROOT = "/sdcard";
+static constexpr const char* SYSTEM_ROOT = "/system";
 
 // We define RECOVERY_API_VERSION in Android.mk, which will be picked up by build system and packed
 // into target_files.zip. Assert the version defined in code and in Android.mk are consistent.
@@ -394,7 +395,7 @@
     return success;
 }
 
-static bool prompt_and_wipe_data(Device* device) {
+static InstallResult prompt_and_wipe_data(Device* device) {
   // Use a single string and let ScreenRecoveryUI handles the wrapping.
   std::vector<std::string> headers{
     "Can't load Android system. Your data may be corrupt. "
@@ -415,13 +416,17 @@
 
     // If ShowMenu() returned RecoveryUI::KeyError::INTERRUPTED, WaitKey() was interrupted.
     if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
-      return false;
+      return INSTALL_KEY_INTERRUPTED;
     }
     if (chosen_item != 1) {
-      return true;  // Just reboot, no wipe; not a failure, user asked for it
+      return INSTALL_SUCCESS;  // Just reboot, no wipe; not a failure, user asked for it
     }
     if (ask_to_wipe_data(device)) {
-      return wipe_data(device);
+      if (wipe_data(device)) {
+        return INSTALL_SUCCESS;
+      } else {
+        return INSTALL_ERROR;
+      }
     }
   }
 }
@@ -848,12 +853,12 @@
       }
       case Device::MOUNT_SYSTEM:
         // the system partition is mounted at /mnt/system
-        if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
+        if (volume_for_mount_point(SYSTEM_ROOT) == nullptr) {
           if (ensure_path_mounted_at("/", "/mnt/system") != -1) {
             ui->Print("Mounted /system.\n");
           }
         } else {
-          if (ensure_path_mounted_at("/system", "/mnt/system") != -1) {
+          if (ensure_path_mounted_at(SYSTEM_ROOT, "/mnt/system") != -1) {
             ui->Print("Mounted /system.\n");
           }
         }
@@ -1192,12 +1197,15 @@
       status = INSTALL_ERROR;
     }
   } else if (should_prompt_and_wipe_data) {
+    // Trigger the logging to capture the cause, even if user chooses to not wipe data.
+    modified_flash = true;
+
     ui->ShowText(true);
     ui->SetBackground(RecoveryUI::ERROR);
-    if (!prompt_and_wipe_data(device)) {
-      status = INSTALL_ERROR;
+    status = prompt_and_wipe_data(device);
+    if (status != INSTALL_KEY_INTERRUPTED) {
+      ui->ShowText(false);
     }
-    ui->ShowText(false);
   } else if (should_wipe_cache) {
     if (!wipe_cache(false, device)) {
       status = INSTALL_ERROR;
diff --git a/recovery_main.cpp b/recovery_main.cpp
index c3168fc..7835094 100644
--- a/recovery_main.cpp
+++ b/recovery_main.cpp
@@ -364,7 +364,8 @@
         std::string option = OPTIONS[option_index].name;
         if (option == "locale") {
           locale = optarg;
-        } else if (option == "fastboot") {
+        } else if (option == "fastboot" &&
+                   android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
           fastboot = true;
         }
         break;
@@ -425,6 +426,10 @@
     device->RemoveMenuItemForAction(Device::WIPE_CACHE);
   }
 
+  if (!android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
+    device->RemoveMenuItemForAction(Device::ENTER_FASTBOOT);
+  }
+
   ui->SetBackground(RecoveryUI::NONE);
   if (show_text) ui->ShowText(true);
 
diff --git a/screen_ui.cpp b/screen_ui.cpp
index 391dedb..181d58e 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -54,15 +54,23 @@
   return tv.tv_sec + tv.tv_usec / 1000000.0;
 }
 
-Menu::Menu(bool scrollable, size_t max_items, size_t max_length,
-           const std::vector<std::string>& headers, const std::vector<std::string>& items,
-           size_t initial_selection)
-    : scrollable_(scrollable),
+Menu::Menu(size_t initial_selection, const DrawInterface& draw_func)
+    : selection_(initial_selection), draw_funcs_(draw_func) {}
+
+size_t Menu::selection() const {
+  return selection_;
+}
+
+TextMenu::TextMenu(bool scrollable, size_t max_items, size_t max_length,
+                   const std::vector<std::string>& headers, const std::vector<std::string>& items,
+                   size_t initial_selection, int char_height, const DrawInterface& draw_funcs)
+    : Menu(initial_selection, draw_funcs),
+      scrollable_(scrollable),
       max_display_items_(max_items),
       max_item_length_(max_length),
       text_headers_(headers),
       menu_start_(0),
-      selection_(initial_selection) {
+      char_height_(char_height) {
   CHECK_LE(max_items, static_cast<size_t>(std::numeric_limits<int>::max()));
 
   // It's fine to have more entries than text_rows_ if scrollable menu is supported.
@@ -74,29 +82,29 @@
   CHECK(!text_items_.empty());
 }
 
-const std::vector<std::string>& Menu::text_headers() const {
+const std::vector<std::string>& TextMenu::text_headers() const {
   return text_headers_;
 }
 
-std::string Menu::TextItem(size_t index) const {
+std::string TextMenu::TextItem(size_t index) const {
   CHECK_LT(index, text_items_.size());
 
   return text_items_[index];
 }
 
-size_t Menu::MenuStart() const {
+size_t TextMenu::MenuStart() const {
   return menu_start_;
 }
 
-size_t Menu::MenuEnd() const {
+size_t TextMenu::MenuEnd() const {
   return std::min(ItemsCount(), menu_start_ + max_display_items_);
 }
 
-size_t Menu::ItemsCount() const {
+size_t TextMenu::ItemsCount() const {
   return text_items_.size();
 }
 
-bool Menu::ItemsOverflow(std::string* cur_selection_str) const {
+bool TextMenu::ItemsOverflow(std::string* cur_selection_str) const {
   if (!scrollable_ || ItemsCount() <= max_display_items_) {
     return false;
   }
@@ -107,7 +115,7 @@
 }
 
 // TODO(xunchang) modify the function parameters to button up & down.
-int Menu::Select(int sel) {
+int TextMenu::Select(int sel) {
   CHECK_LE(ItemsCount(), static_cast<size_t>(std::numeric_limits<int>::max()));
   int count = ItemsCount();
 
@@ -140,6 +148,151 @@
   return selection_;
 }
 
+int TextMenu::DrawHeader(int x, int y) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::HEADER);
+  if (!scrollable()) {
+    offset += draw_funcs_.DrawWrappedTextLines(x, y + offset, text_headers());
+  } else {
+    offset += draw_funcs_.DrawTextLines(x, y + offset, text_headers());
+    // Show the current menu item number in relation to total number if items don't fit on the
+    // screen.
+    std::string cur_selection_str;
+    if (ItemsOverflow(&cur_selection_str)) {
+      offset += draw_funcs_.DrawTextLine(x, y + offset, cur_selection_str, true);
+    }
+  }
+
+  return offset;
+}
+
+int TextMenu::DrawItems(int x, int y, int screen_width, bool long_press) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::MENU);
+  // Do not draw the horizontal rule for wear devices.
+  if (!scrollable()) {
+    offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4;
+  }
+  for (size_t i = MenuStart(); i < MenuEnd(); ++i) {
+    bool bold = false;
+    if (i == selection()) {
+      // Draw the highlight bar.
+      draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG);
+
+      int bar_height = char_height_ + 4;
+      draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height);
+
+      // Bold white text for the selected item.
+      draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
+      bold = true;
+    }
+    offset += draw_funcs_.DrawTextLine(x, y + offset, TextItem(i), bold);
+
+    draw_funcs_.SetColor(UIElement::MENU);
+  }
+  offset += draw_funcs_.DrawHorizontalRule(y + offset);
+
+  return offset;
+}
+
+GraphicMenu::GraphicMenu(size_t max_width, size_t max_height, GRSurface* graphic_headers,
+                         const std::vector<GRSurface*>& graphic_items, size_t initial_selection,
+                         const DrawInterface& draw_funcs)
+    : Menu(initial_selection, draw_funcs),
+      max_width_(max_width),
+      max_height_(max_height),
+      graphic_headers_(graphic_headers),
+      graphic_items_(graphic_items) {}
+
+int GraphicMenu::Select(int sel) {
+  CHECK_LE(graphic_items_.size(), static_cast<size_t>(std::numeric_limits<int>::max()));
+  int count = graphic_items_.size();
+
+  // Wraps the selection at boundary if the menu is not scrollable.
+  if (sel < 0) {
+    selection_ = count - 1;
+  } else if (sel >= count) {
+    selection_ = 0;
+  } else {
+    selection_ = sel;
+  }
+
+  return selection_;
+}
+
+int GraphicMenu::DrawHeader(int x, int y) const {
+  draw_funcs_.DrawTextIcon(x, y, graphic_headers_);
+  return graphic_headers_->height;
+}
+
+int GraphicMenu::DrawItems(int x, int y, int screen_width, bool long_press) const {
+  int offset = 0;
+
+  draw_funcs_.SetColor(UIElement::MENU);
+  offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4;
+
+  for (size_t i = 0; i < graphic_items_.size(); i++) {
+    auto& item = graphic_items_[i];
+    if (i == selection_) {
+      draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG);
+
+      int bar_height = item->height + 4;
+      draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height);
+
+      // Bold white text for the selected item.
+      draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
+    }
+    draw_funcs_.DrawTextIcon(x, y + offset, item);
+    offset += item->height;
+
+    draw_funcs_.SetColor(UIElement::MENU);
+  }
+
+  return offset;
+}
+
+bool GraphicMenu::Validate() const {
+  int offset = 0;
+  if (!ValidateGraphicSurface(offset, graphic_headers_)) {
+    return false;
+  }
+  offset += graphic_headers_->height;
+
+  for (const auto& item : graphic_items_) {
+    if (!ValidateGraphicSurface(offset, item)) {
+      return false;
+    }
+    offset += item->height;
+  }
+
+  return true;
+}
+
+bool GraphicMenu::ValidateGraphicSurface(int y, const GRSurface* surface) const {
+  if (!surface) {
+    fprintf(stderr, "Graphic surface can not be null");
+    return false;
+  }
+
+  if (surface->pixel_bytes != 1 || surface->width != surface->row_bytes) {
+    fprintf(stderr, "Invalid graphic surface, pixel bytes: %d, width: %d row_bytes: %d",
+            surface->pixel_bytes, surface->width, surface->row_bytes);
+    return false;
+  }
+
+  if (surface->width > max_width_ || surface->height > max_height_ - y) {
+    fprintf(stderr,
+            "Graphic surface doesn't fit into the screen. width: %d, height: %d, max_width: %zu,"
+            " max_height: %zu, vertical offset: %d\n",
+            surface->width, surface->height, max_width_, max_height_, y);
+    return false;
+  }
+
+  return true;
+}
+
 ScreenRecoveryUI::ScreenRecoveryUI() : ScreenRecoveryUI(false) {}
 
 constexpr int kDefaultMarginHeight = 0;
@@ -332,26 +485,26 @@
 
 void ScreenRecoveryUI::SetColor(UIElement e) const {
   switch (e) {
-    case INFO:
+    case UIElement::INFO:
       gr_color(249, 194, 0, 255);
       break;
-    case HEADER:
+    case UIElement::HEADER:
       gr_color(247, 0, 6, 255);
       break;
-    case MENU:
-    case MENU_SEL_BG:
+    case UIElement::MENU:
+    case UIElement::MENU_SEL_BG:
       gr_color(0, 106, 157, 255);
       break;
-    case MENU_SEL_BG_ACTIVE:
+    case UIElement::MENU_SEL_BG_ACTIVE:
       gr_color(0, 156, 100, 255);
       break;
-    case MENU_SEL_FG:
+    case UIElement::MENU_SEL_FG:
       gr_color(255, 255, 255, 255);
       break;
-    case LOG:
+    case UIElement::LOG:
       gr_color(196, 196, 196, 255);
       break;
-    case TEXT_FILL:
+    case UIElement::TEXT_FILL:
       gr_color(0, 0, 0, 160);
       break;
     default:
@@ -384,7 +537,7 @@
   int text_x = margin_width_;
   int line_spacing = gr_sys_font()->char_height;  // Put some extra space between images.
   // Write the header and descriptive texts.
-  SetColor(INFO);
+  SetColor(UIElement::INFO);
   std::string header = "Show background text image";
   text_y += DrawTextLine(text_x, text_y, header, true);
   std::string locale_selection = android::base::StringPrintf(
@@ -400,7 +553,7 @@
   // Iterate through the text images and display them in order for the current locale.
   for (const auto& p : surfaces) {
     text_y += line_spacing;
-    SetColor(LOG);
+    SetColor(UIElement::LOG);
     text_y += DrawTextLine(text_x, text_y, p.first, false);
     gr_color(255, 255, 255, 255);
     gr_texticon(text_x, text_y, p.second.get());
@@ -547,7 +700,7 @@
     static constexpr int kMenuIndent = 4;
     int x = margin_width_ + kMenuIndent;
 
-    SetColor(INFO);
+    SetColor(UIElement::INFO);
 
     for (size_t i = 0; i < title_lines_.size(); i++) {
       y += DrawTextLine(x, y, title_lines_[i], i == 0);
@@ -555,50 +708,13 @@
 
     y += DrawTextLines(x, y, help_message);
 
-    // Draw menu header.
-    SetColor(HEADER);
-    if (!menu_->scrollable()) {
-      y += DrawWrappedTextLines(x, y, menu_->text_headers());
-    } else {
-      y += DrawTextLines(x, y, menu_->text_headers());
-      // Show the current menu item number in relation to total number if items don't fit on the
-      // screen.
-      std::string cur_selection_str;
-      if (menu_->ItemsOverflow(&cur_selection_str)) {
-        y += DrawTextLine(x, y, cur_selection_str, true);
-      }
-    }
-
-    // Draw menu items.
-    SetColor(MENU);
-    // Do not draw the horizontal rule for wear devices.
-    if (!menu_->scrollable()) {
-      y += DrawHorizontalRule(y) + 4;
-    }
-    for (size_t i = menu_->MenuStart(); i < menu_->MenuEnd(); ++i) {
-      bool bold = false;
-      if (i == static_cast<size_t>(menu_->selection())) {
-        // Draw the highlight bar.
-        SetColor(IsLongPress() ? MENU_SEL_BG_ACTIVE : MENU_SEL_BG);
-
-        int bar_height = char_height_ + 4;
-        DrawHighlightBar(0, y - 2, ScreenWidth(), bar_height);
-
-        // Bold white text for the selected item.
-        SetColor(MENU_SEL_FG);
-        bold = true;
-      }
-
-      y += DrawTextLine(x, y, menu_->TextItem(i), bold);
-
-      SetColor(MENU);
-    }
-    y += DrawHorizontalRule(y);
+    y += menu_->DrawHeader(x, y);
+    y += menu_->DrawItems(x, y, ScreenWidth(), IsLongPress());
   }
 
   // Display from the bottom up, until we hit the top of the screen, the bottom of the menu, or
   // we've displayed the entire text buffer.
-  SetColor(LOG);
+  SetColor(UIElement::LOG);
   int row = text_row_;
   size_t count = 0;
   for (int ty = ScreenHeight() - margin_height_ - char_height_; ty >= y && count < text_rows_;
@@ -992,8 +1108,8 @@
                                  const std::vector<std::string>& items, size_t initial_selection) {
   std::lock_guard<std::mutex> lg(updateMutex);
   if (text_rows_ > 0 && text_cols_ > 1) {
-    menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
-                                   initial_selection);
+    menu_ = std::make_unique<TextMenu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
+                                       initial_selection, char_height_, *this);
     update_screen_locked();
   }
 }
diff --git a/screen_ui.h b/screen_ui.h
index f08f4f4..9152887 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -31,23 +31,92 @@
 // From minui/minui.h.
 struct GRSurface;
 
-// This class maintains the menu selection and display of the screen ui.
+enum class UIElement {
+  HEADER,
+  MENU,
+  MENU_SEL_BG,
+  MENU_SEL_BG_ACTIVE,
+  MENU_SEL_FG,
+  LOG,
+  TEXT_FILL,
+  INFO
+};
+
+// Interface to draw the UI elements on the screen.
+class DrawInterface {
+ public:
+  virtual ~DrawInterface() = default;
+
+  // Sets the color to the predefined value for |element|.
+  virtual void SetColor(UIElement element) const = 0;
+
+  // Draws a highlight bar at (x, y) - (x + width, y + height).
+  virtual void DrawHighlightBar(int x, int y, int width, int height) const = 0;
+
+  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
+  virtual int DrawHorizontalRule(int y) const = 0;
+
+  // Draws a line of text. Returns the offset it should be moving along Y-axis.
+  virtual int DrawTextLine(int x, int y, const std::string& line, bool bold) const = 0;
+
+  // Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
+  virtual void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
+                           int dy) const = 0;
+
+  // Draws rectangle at (x, y) - (x + w, y + h).
+  virtual void DrawFill(int x, int y, int w, int h) const = 0;
+
+  // Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
+  virtual void DrawTextIcon(int x, int y, GRSurface* surface) const = 0;
+
+  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
+  virtual int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const = 0;
+
+  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It
+  // keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving
+  // along Y-axis.
+  virtual int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const = 0;
+};
+
+// Interface for classes that maintain the menu selection and display.
 class Menu {
  public:
+  virtual ~Menu() = default;
+  // Returns the current menu selection.
+  size_t selection() const;
+  // Sets the current selection to |sel|. Handle the overflow cases depending on if the menu is
+  // scrollable.
+  virtual int Select(int sel) = 0;
+  // Displays the menu headers on the screen at offset x, y
+  virtual int DrawHeader(int x, int y) const = 0;
+  // Iterates over the menu items and displays each of them at offset x, y.
+  virtual int DrawItems(int x, int y, int screen_width, bool long_press) const = 0;
+
+ protected:
+  Menu(size_t initial_selection, const DrawInterface& draw_func);
+  // Current menu selection.
+  size_t selection_;
+  // Reference to the class that implements all the draw functions.
+  const DrawInterface& draw_funcs_;
+};
+
+// This class uses strings as the menu header and items.
+class TextMenu : public Menu {
+ public:
   // Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial
   // selection to |initial_selection|.
-  Menu(bool scrollable, size_t max_items, size_t max_length,
-       const std::vector<std::string>& headers, const std::vector<std::string>& items,
-       size_t initial_selection);
+  TextMenu(bool scrollable, size_t max_items, size_t max_length,
+           const std::vector<std::string>& headers, const std::vector<std::string>& items,
+           size_t initial_selection, int char_height, const DrawInterface& draw_funcs);
+
+  int Select(int sel) override;
+  int DrawHeader(int x, int y) const override;
+  int DrawItems(int x, int y, int screen_width, bool long_press) const override;
 
   bool scrollable() const {
     return scrollable_;
   }
 
-  size_t selection() const {
-    return selection_;
-  }
-
   // Returns count of menu items.
   size_t ItemsCount() const;
 
@@ -75,10 +144,6 @@
   // |cur_selection_str| if the items exceed the screen limit.
   bool ItemsOverflow(std::string* cur_selection_str) const;
 
-  // Sets the current selection to |sel|. Handle the overflow cases depending on if the menu is
-  // scrollable.
-  int Select(int sel);
-
  private:
   // The menu is scrollable to display more items. Used on wear devices who have smaller screens.
   const bool scrollable_;
@@ -92,25 +157,45 @@
   std::vector<std::string> text_items_;
   // The first item to display on the screen.
   size_t menu_start_;
-  // Current menu selection.
-  size_t selection_;
+
+  // Height in pixels of each character.
+  int char_height_;
+};
+
+// This class uses GRSurfaces* 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(size_t max_width, size_t max_height, GRSurface* graphic_headers,
+              const std::vector<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
+  // defined by |max_width_| and |max_height_|.
+  bool Validate() const;
+
+ private:
+  // Returns true if |surface| fits on the screen with a vertical offset |y|.
+  bool ValidateGraphicSurface(int y, const GRSurface* surface) const;
+
+  const size_t max_width_;
+  const size_t max_height_;
+
+  // 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_;
 };
 
 // Implementation of RecoveryUI appropriate for devices with a screen
 // (shows an icon + a progress bar, text logging, menu, etc.)
-class ScreenRecoveryUI : public RecoveryUI {
+class ScreenRecoveryUI : public RecoveryUI, public DrawInterface {
  public:
-  enum UIElement {
-    HEADER,
-    MENU,
-    MENU_SEL_BG,
-    MENU_SEL_BG_ACTIVE,
-    MENU_SEL_FG,
-    LOG,
-    TEXT_FILL,
-    INFO
-  };
-
   ScreenRecoveryUI();
   explicit ScreenRecoveryUI(bool scrollable_menu);
   ~ScreenRecoveryUI() override;
@@ -149,8 +234,6 @@
 
   void Redraw();
 
-  void SetColor(UIElement e) const;
-
   // Checks the background text image, for debugging purpose. It iterates the locales embedded in
   // the on-device resource files and shows the localized text, for manual inspection.
   void CheckBackgroundTextImages();
@@ -212,24 +295,16 @@
   // Returns pixel height of draw buffer.
   virtual int ScreenHeight() const;
 
-  // Draws a highlight bar at (x, y) - (x + width, y + height).
-  virtual void DrawHighlightBar(int x, int y, int width, int height) const;
-  // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
-  virtual int DrawHorizontalRule(int y) const;
-  // Draws a line of text. Returns the offset it should be moving along Y-axis.
-  virtual int DrawTextLine(int x, int y, const std::string& line, bool bold) const;
-  // Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
-  virtual void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const;
-  // Draws rectangle at (x, y) - (x + w, y + h).
-  virtual void DrawFill(int x, int y, int w, int h) const;
-  // Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
-  virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
-  // Draws multiple text lines. Returns the offset it should be moving along Y-axis.
-  int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const;
-  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It
-  // keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving
-  // along Y-axis.
-  int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const;
+  // Implementation of the draw functions in DrawInterface.
+  void SetColor(UIElement e) const override;
+  void DrawHighlightBar(int x, int y, int width, int height) const override;
+  int DrawHorizontalRule(int y) const override;
+  void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const override;
+  void DrawFill(int x, int y, int w, int h) const override;
+  void DrawTextIcon(int x, int y, GRSurface* surface) const override;
+  int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
+  int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const override;
+  int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const override;
 
   Icon currentIcon;
 
diff --git a/tests/Android.bp b/tests/Android.bp
index ab4d31d..2cfc325 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -45,7 +45,7 @@
             static_libs: [
                 "libutils",
             ],
-        }
+        },
     },
 }
 
@@ -93,12 +93,14 @@
     "libhidlbase",
     "libhidltransport",
     "libhwbinder",
+    "libbinderthreadstate",
     "libvndksupport",
     "libtinyxml2",
 ]
 
 cc_test {
     name: "recovery_unit_test",
+    isolated: true,
 
     defaults: [
         "recovery_test_defaults",
@@ -117,7 +119,6 @@
         "libotautil",
         "libupdater",
         "libgtest_prod",
-        "libBionicGtestMain",
     ],
 
     data: ["testdata/*"],
@@ -125,6 +126,7 @@
 
 cc_test {
     name: "recovery_manual_test",
+    isolated: true,
 
     defaults: [
         "recovery_test_defaults",
@@ -135,14 +137,11 @@
     srcs: [
         "manual/recovery_test.cpp",
     ],
-
-    static_libs: [
-        "libBionicGtestMain",
-    ],
 }
 
 cc_test {
     name: "recovery_component_test",
+    isolated: true,
 
     defaults: [
         "recovery_test_defaults",
@@ -159,7 +158,6 @@
         "libupdater",
         "libupdate_verifier",
         "libprotobuf-cpp-lite",
-        "libBionicGtestMain",
     ],
 
     data: [
@@ -170,6 +168,7 @@
 
 cc_test_host {
     name: "recovery_host_test",
+    isolated: true,
 
     defaults: [
         "recovery_test_defaults",
@@ -193,7 +192,6 @@
         "libdivsufsort64",
         "libdivsufsort",
         "libz",
-        "libBionicGtestMain",
     ],
 
     data: ["testdata/*"],
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index a970716..2420c27 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -16,6 +16,7 @@
 
 #include <update_verifier/update_verifier.h>
 
+#include <functional>
 #include <string>
 #include <unordered_map>
 #include <vector>
@@ -29,25 +30,49 @@
 
 #include "care_map.pb.h"
 
+using namespace std::string_literals;
+
 class UpdateVerifierTest : public ::testing::Test {
  protected:
   void SetUp() override {
     std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
     verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing");
+
+    care_map_prefix_ = care_map_dir_.path + "/care_map"s;
+    care_map_pb_ = care_map_dir_.path + "/care_map.pb"s;
+    care_map_txt_ = care_map_dir_.path + "/care_map.txt"s;
+    // Overrides the the care_map_prefix.
+    verifier_.set_care_map_prefix(care_map_prefix_);
+
+    property_id_ = "ro.build.fingerprint";
+    fingerprint_ = android::base::GetProperty(property_id_, "");
+    // Overrides the property_reader if we cannot read the given property on the device.
+    if (fingerprint_.empty()) {
+      fingerprint_ = "mock_fingerprint";
+      verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; });
+    }
+  }
+
+  void TearDown() override {
+    unlink(care_map_pb_.c_str());
+    unlink(care_map_txt_.c_str());
   }
 
   // Returns a serialized string of the proto3 message according to the given partition info.
   std::string ConstructProto(
       std::vector<std::unordered_map<std::string, std::string>>& partitions) {
-    UpdateVerifier::CareMap result;
+    recovery_update_verifier::CareMap result;
     for (const auto& partition : partitions) {
-      UpdateVerifier::CareMap::PartitionInfo info;
+      recovery_update_verifier::CareMap::PartitionInfo info;
       if (partition.find("name") != partition.end()) {
         info.set_name(partition.at("name"));
       }
       if (partition.find("ranges") != partition.end()) {
         info.set_ranges(partition.at("ranges"));
       }
+      if (partition.find("id") != partition.end()) {
+        info.set_id(partition.at("id"));
+      }
       if (partition.find("fingerprint") != partition.end()) {
         info.set_fingerprint(partition.at("fingerprint"));
       }
@@ -59,12 +84,19 @@
   }
 
   bool verity_supported;
-  TemporaryFile care_map_file;
+  UpdateVerifier verifier_;
+
+  TemporaryDir care_map_dir_;
+  std::string care_map_prefix_;
+  std::string care_map_pb_;
+  std::string care_map_txt_;
+
+  std::string property_id_;
+  std::string fingerprint_;
 };
 
 TEST_F(UpdateVerifierTest, verify_image_no_care_map) {
-  // Non-existing care_map is allowed.
-  ASSERT_TRUE(verify_image("/doesntexist"));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_smoke) {
@@ -75,25 +107,27 @@
   }
 
   std::string content = "system\n2,0,1";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_file.path));
-  ASSERT_TRUE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
 
   // Leading and trailing newlines should be accepted.
-  ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", care_map_file.path));
-  ASSERT_TRUE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", care_map_txt_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_empty_care_map) {
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_wrong_lines) {
   // The care map file can have only 2 / 4 / 6 lines.
-  ASSERT_TRUE(android::base::WriteStringToFile("line1", care_map_file.path));
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("line1", care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 
-  ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", care_map_file.path));
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) {
@@ -104,8 +138,8 @@
   }
 
   std::string content = "system\n2,1,0";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_file.path));
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) {
@@ -116,8 +150,8 @@
   }
 
   std::string content = "/dev/block/bootdevice/by-name/system\n2,1,0";
-  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_file.path));
-  ASSERT_TRUE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) {
@@ -128,12 +162,18 @@
   }
 
   std::vector<std::unordered_map<std::string, std::string>> partitions = {
-    { { "name", "system" }, { "ranges", "2,0,1" } },
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
   };
 
   std::string proto = ConstructProto(partitions);
-  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_file.path));
-  ASSERT_TRUE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_TRUE(verifier_.ParseCareMap());
+  ASSERT_TRUE(verifier_.VerifyPartitions());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) {
@@ -144,12 +184,16 @@
   }
 
   std::vector<std::unordered_map<std::string, std::string>> partitions = {
-    { { "ranges", "2,0,1" } },
+    {
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
   };
 
   std::string proto = ConstructProto(partitions);
-  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_file.path));
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
 
 TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) {
@@ -160,10 +204,55 @@
   }
 
   std::vector<std::unordered_map<std::string, std::string>> partitions = {
-    { { "name", "system" }, { "ranges", "3,0,1" } },
+    {
+        { "name", "system" },
+        { "ranges", "3,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", fingerprint_ },
+    },
   };
 
   std::string proto = ConstructProto(partitions);
-  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_file.path));
-  ASSERT_FALSE(verify_image(care_map_file.path));
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) {
+  // This test relies on dm-verity support.
+  if (!verity_supported) {
+    GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+    return;
+  }
+
+  std::vector<std::unordered_map<std::string, std::string>> partitions = {
+    {
+        { "name", "system" },
+        { "ranges", "2,0,1" },
+        { "id", property_id_ },
+        { "fingerprint", "unsupported_fingerprint" },
+    },
+  };
+
+  std::string proto = ConstructProto(partitions);
+  ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+  ASSERT_FALSE(verifier_.ParseCareMap());
 }
diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp
index 3246ecd..c460cbe 100644
--- a/tests/component/verifier_test.cpp
+++ b/tests/component/verifier_test.cpp
@@ -27,6 +27,7 @@
 #include <android-base/file.h>
 #include <android-base/stringprintf.h>
 #include <android-base/test_utils.h>
+#include <android-base/unique_fd.h>
 #include <gtest/gtest.h>
 
 #include "common/test_constants.h"
@@ -35,6 +36,89 @@
 
 using namespace std::string_literals;
 
+static void LoadKeyFromFile(const std::string& file_name, Certificate* cert) {
+  std::string testkey_string;
+  ASSERT_TRUE(android::base::ReadFileToString(file_name, &testkey_string));
+  ASSERT_TRUE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), cert));
+}
+
+static void VerifyPackageWithCertificate(const std::string& name, Certificate&& cert) {
+  std::string package = from_testdata_base(name);
+  MemMapping memmap;
+  if (!memmap.MapFile(package)) {
+    FAIL() << "Failed to mmap " << package << ": " << strerror(errno) << "\n";
+  }
+
+  std::vector<Certificate> certs;
+  certs.emplace_back(std::move(cert));
+  ASSERT_EQ(VERIFY_SUCCESS, verify_file(memmap.addr, memmap.length, certs));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_failure) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  std::string testkey_string;
+  ASSERT_TRUE(
+      android::base::ReadFileToString(from_testdata_base("testkey_v1.txt"), &testkey_string));
+  ASSERT_FALSE(LoadCertificateFromBuffer(
+      std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), &cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v1.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithCertificate("otasigned_v1.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v2.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithCertificate("otasigned_v2.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent3) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithCertificate("otasigned_v3.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent65537) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v4.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type);
+  ASSERT_EQ(nullptr, cert.ec);
+
+  VerifyPackageWithCertificate("otasigned_v4.zip", std::move(cert));
+}
+
+TEST(VerifierTest, LoadCertificateFromBuffer_sha256_ec256bits) {
+  Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr);
+  LoadKeyFromFile(from_testdata_base("testkey_v5.x509.pem"), &cert);
+
+  ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len);
+  ASSERT_EQ(Certificate::KEY_TYPE_EC, cert.key_type);
+  ASSERT_EQ(nullptr, cert.rsa);
+
+  VerifyPackageWithCertificate("otasigned_v5.zip", std::move(cert));
+}
+
 class VerifierTest : public testing::TestWithParam<std::vector<std::string>> {
  protected:
   void SetUp() override {
diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp
new file mode 100644
index 0000000..8061f3b
--- /dev/null
+++ b/tests/unit/parse_install_logs_test.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "otautil/parse_install_logs.h"
+
+TEST(ParseInstallLogsTest, EmptyFile) {
+  TemporaryFile last_install;
+
+  auto metrics = ParseLastInstall(last_install.path);
+  ASSERT_TRUE(metrics.empty());
+}
+
+TEST(ParseInstallLogsTest, SideloadSmoke) {
+  TemporaryFile last_install;
+  ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path));
+  auto metrics = ParseLastInstall(last_install.path);
+  ASSERT_EQ(metrics.end(), metrics.find("ota_sideload"));
+
+  ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path));
+  metrics = ParseLastInstall(last_install.path);
+  ASSERT_NE(metrics.end(), metrics.find("ota_sideload"));
+}
+
+TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) {
+  std::vector<std::string> lines = {
+    "/sideload/package.zip",
+    "0",
+    "time_total: 300",
+    "uncrypt_time: 40",
+    "source_build: 4973410",
+    "bytes_written_system: " + std::to_string(1200 * 1024 * 1024),
+    "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024),
+    "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024),
+    "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024),
+    "temperature_start: 37000",
+    "temperature_end: 38000",
+    "temperature_max: 39000",
+    "error: 22",
+    "cause: 55",
+  };
+
+  auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+  std::map<std::string, int64_t> expected_result = {
+    { "ota_time_total", 300 },         { "ota_uncrypt_time", 40 },
+    { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 },
+    { "ota_stashed_in_MiBs", 350 },    { "ota_temperature_start", 37000 },
+    { "ota_temperature_end", 38000 },  { "ota_temperature_max", 39000 },
+    { "ota_non_ab_error_code", 22 },   { "ota_non_ab_cause_code", 55 },
+  };
+
+  ASSERT_EQ(expected_result, metrics);
+}
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
index 7d97a00..ec26950 100644
--- a/tests/unit/screen_ui_test.cpp
+++ b/tests/unit/screen_ui_test.cpp
@@ -38,8 +38,39 @@
 static const std::vector<std::string> HEADERS{ "header" };
 static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" };
 
-TEST(ScreenUITest, StartPhoneMenuSmoke) {
-  Menu menu(false, 10, 20, HEADERS, ITEMS, 0);
+// TODO(xunchang) check if some draw functions are called when drawing menus.
+class MockDrawFunctions : public DrawInterface {
+  void SetColor(UIElement /* element */) const override {}
+  void DrawHighlightBar(int /* x */, int /* y */, int /* width */,
+                        int /* height */) const override {};
+  int DrawHorizontalRule(int /* y */) const override {
+    return 0;
+  };
+  int DrawTextLine(int /* x */, int /* y */, const std::string& /* line */,
+                   bool /* bold */) const override {
+    return 0;
+  };
+  void DrawSurface(GRSurface* /* surface */, int /* sx */, int /* sy */, int /* w */, int /* h */,
+                   int /* dx */, int /* dy */) const override {};
+  void DrawFill(int /* x */, int /* y */, int /* w */, int /* h */) const override {};
+  void DrawTextIcon(int /* x */, int /* y */, GRSurface* /* surface */) const override {};
+  int DrawTextLines(int /* x */, int /* y */,
+                    const std::vector<std::string>& /* lines */) const override {
+    return 0;
+  };
+  int DrawWrappedTextLines(int /* x */, int /* y */,
+                           const std::vector<std::string>& /* lines */) const override {
+    return 0;
+  };
+};
+
+class ScreenUITest : public testing::Test {
+ protected:
+  MockDrawFunctions draw_funcs_;
+};
+
+TEST_F(ScreenUITest, StartPhoneMenuSmoke) {
+  TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
   ASSERT_FALSE(menu.scrollable());
   ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
   ASSERT_EQ(5u, menu.ItemsCount());
@@ -53,8 +84,8 @@
   ASSERT_EQ(0, menu.selection());
 }
 
-TEST(ScreenUITest, StartWearMenuSmoke) {
-  Menu menu(true, 10, 8, HEADERS, ITEMS, 1);
+TEST_F(ScreenUITest, StartWearMenuSmoke) {
+  TextMenu menu(true, 10, 8, HEADERS, ITEMS, 1, 20, draw_funcs_);
   ASSERT_TRUE(menu.scrollable());
   ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
   ASSERT_EQ(5u, menu.ItemsCount());
@@ -69,8 +100,8 @@
   ASSERT_EQ(1, menu.selection());
 }
 
-TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
-  Menu menu(false, 1, 20, HEADERS, ITEMS, 0);
+TEST_F(ScreenUITest, StartPhoneMenuItemsOverflow) {
+  TextMenu menu(false, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
   ASSERT_FALSE(menu.scrollable());
   ASSERT_EQ(1u, menu.ItemsCount());
 
@@ -84,8 +115,8 @@
   ASSERT_EQ(1u, menu.MenuEnd());
 }
 
-TEST(ScreenUITest, StartWearMenuItemsOverflow) {
-  Menu menu(true, 1, 20, HEADERS, ITEMS, 0);
+TEST_F(ScreenUITest, StartWearMenuItemsOverflow) {
+  TextMenu menu(true, 1, 20, HEADERS, ITEMS, 0, 20, draw_funcs_);
   ASSERT_TRUE(menu.scrollable());
   ASSERT_EQ(5u, menu.ItemsCount());
 
@@ -101,9 +132,9 @@
   ASSERT_EQ(1u, menu.MenuEnd());
 }
 
-TEST(ScreenUITest, PhoneMenuSelectSmoke) {
+TEST_F(ScreenUITest, PhoneMenuSelectSmoke) {
   int sel = 0;
-  Menu menu(false, 10, 20, HEADERS, ITEMS, sel);
+  TextMenu menu(false, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
   // Mimic down button 10 times (2 * items size)
   for (int i = 0; i < 10; i++) {
     sel = menu.Select(++sel);
@@ -130,9 +161,9 @@
   }
 }
 
-TEST(ScreenUITest, WearMenuSelectSmoke) {
+TEST_F(ScreenUITest, WearMenuSelectSmoke) {
   int sel = 0;
-  Menu menu(true, 10, 20, HEADERS, ITEMS, sel);
+  TextMenu menu(true, 10, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
   // Mimic pressing down button 10 times (2 * items size)
   for (int i = 0; i < 10; i++) {
     sel = menu.Select(++sel);
@@ -159,9 +190,9 @@
   }
 }
 
-TEST(ScreenUITest, WearMenuSelectItemsOverflow) {
+TEST_F(ScreenUITest, WearMenuSelectItemsOverflow) {
   int sel = 1;
-  Menu menu(true, 3, 20, HEADERS, ITEMS, sel);
+  TextMenu menu(true, 3, 20, HEADERS, ITEMS, sel, 20, draw_funcs_);
   ASSERT_EQ(5u, menu.ItemsCount());
 
   // Scroll the menu to the end, and check the start & end of menu.
diff --git a/tools/image_generator/Android.bp b/tools/image_generator/Android.bp
new file mode 100644
index 0000000..3f718fe
--- /dev/null
+++ b/tools/image_generator/Android.bp
@@ -0,0 +1,23 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+    name: "RecoveryImageGenerator",
+
+    manifest: "ImageGenerator.mf",
+
+    srcs: [
+        "ImageGenerator.java",
+    ],
+}
\ No newline at end of file
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
new file mode 100644
index 0000000..f226216
--- /dev/null
+++ b/tools/image_generator/ImageGenerator.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.recovery.tools;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.FontFormatException;
+import java.awt.FontMetrics;
+import java.awt.Graphics2D;
+import java.awt.RenderingHints;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.StringTokenizer;
+
+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.
+ */
+public class ImageGenerator {
+  // Initial height of the image to draw.
+  private static final int INITIAL_HEIGHT = 20000;
+
+  private static final float DEFAULT_FONT_SIZE = 40;
+
+  // 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 current height in pixels of our image. We will adjust the value when drawing more texts.
+  private int mImageHeight;
+
+  // The current vertical offset in pixels to draw the top edge of new text strings.
+  private int mVerticalOffset;
+
+  // The font size to draw the texts.
+  private final float mFontSize;
+
+  // The name description of the text to localize. It's used to find the translated strings in the
+  // resource file.
+  private final String mTextName;
+
+  // The directory that contains all the needed font files (e.g. ttf, otf, ttc files).
+  private final String mFontDirPath;
+
+  // 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");
+  }};
+
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * 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);
+
+    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();
+      }
+    }
+
+    throw new LocalizedStringNotFoundException(textName + " not found in "
+        + resourceFile.getName());
+  }
+
+  /**
+   * Constructs the locale from the name of the resource file.
+   */
+  private Locale getLocaleFromFilename(String filename) throws IOException {
+    // Gets the locale string by trimming the top "values-".
+    String localeString = filename.substring(7);
+    if (localeString.matches("[A-Za-z]+")) {
+      return Locale.forLanguageTag(localeString);
+    }
+    if (localeString.matches("[A-Za-z]+-r[A-Za-z]+")) {
+      // "${Language}-r${Region}". e.g. en-rGB
+      String[] tokens = localeString.split("-r");
+      return Locale.forLanguageTag(String.join("-", tokens));
+    }
+    if (localeString.startsWith("b+")) {
+      // The special case of b+sr+Latn, which has the form "b+${Language}+${ScriptName}"
+      String[] tokens = localeString.substring(2).split("\\+");
+      return Locale.forLanguageTag(String.join("-", tokens));
+    }
+
+    throw new IOException("Unrecognized locale string " + localeString);
+  }
+
+  /**
+   * Iterates over the xml files in the format of values-$LOCALE/strings.xml under the resource
+   * directory and collect the translated text.
+   *
+   * @param resourcePath the path to the resource directory
+   *
+   * @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));
+    }
+
+    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());
+
+    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);
+    }
+
+    // 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));
+  }
+
+  public static void printUsage() {
+    System.out.println("Usage: java -jar path_to_jar imageWidth textName fontDirectory"
+        + " resourceDirectory outputFilename");
+  }
+
+  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);
+    }
+
+    // TODO(xunchang) switch to commandline parser
+    int imageWidth = Integer.parseUnsignedInt(args[0]);
+
+    ImageGenerator imageGenerator =
+        new ImageGenerator(imageWidth, args[1], DEFAULT_FONT_SIZE, args[2]);
+
+    Map<Locale, String> localizedStringMap =
+        imageGenerator.readLocalizedStringFromXmls(args[3]);
+    imageGenerator.generateImage(localizedStringMap, args[4]);
+  }
+}
+
diff --git a/tools/image_generator/ImageGenerator.mf b/tools/image_generator/ImageGenerator.mf
new file mode 100644
index 0000000..17712d1
--- /dev/null
+++ b/tools/image_generator/ImageGenerator.mf
@@ -0,0 +1 @@
+Main-Class: com.android.recovery.tools.ImageGenerator
diff --git a/tools/image_generator/README.md b/tools/image_generator/README.md
new file mode 100644
index 0000000..22e32f6
--- /dev/null
+++ b/tools/image_generator/README.md
@@ -0,0 +1,20 @@
+Recovery Image Generator
+-------------------------
+
+This program uses java.awt.Graphics2D to generate the background text files used
+under recovery mode. And thus we don't need to do the manual work by running
+emulators with different dpi.
+
+# Usage:
+  `java -jar path_to_jar imageWidth textName fontDirectory resourceDirectory outputFilename`
+
+# Description of the parameters:
+1. `imageWidth`: The number of pixels per line; and the text strings will be
+   wrapped accordingly.
+2. `textName`: The description of the text string, e.g. "recovery_erasing",
+   "recovery_installing_security"
+3. `fontDirectory`: The directory that contains all the support .ttf | .ttc
+   files, e.g. $OUT/system/fonts/
+4. `resourceDirectory`: The resource directory that contains all the translated
+   strings in xml format, e.g. bootable/recovery/tools/recovery_l10n/res/
+5. `outputFilename`: Path to the generated image.
diff --git a/update_verifier/Android.bp b/update_verifier/Android.bp
index 7a860a1..1b84619 100644
--- a/update_verifier/Android.bp
+++ b/update_verifier/Android.bp
@@ -15,9 +15,8 @@
 cc_defaults {
     name: "update_verifier_defaults",
 
-    cflags: [
-        "-Wall",
-        "-Werror",
+    defaults: [
+        "recovery_defaults",
     ],
 
     local_include_dirs: [
diff --git a/update_verifier/care_map.proto b/update_verifier/care_map.proto
index 442ddd4..15d3afa 100644
--- a/update_verifier/care_map.proto
+++ b/update_verifier/care_map.proto
@@ -16,7 +16,7 @@
 
 syntax = "proto3";
 
-package UpdateVerifier;
+package recovery_update_verifier;
 option optimize_for = LITE_RUNTIME;
 
 message CareMap {
diff --git a/update_verifier/care_map_generator.py b/update_verifier/care_map_generator.py
index 5057ffe..051d98d 100644
--- a/update_verifier/care_map_generator.py
+++ b/update_verifier/care_map_generator.py
@@ -27,32 +27,44 @@
 import care_map_pb2
 
 
-def GenerateCareMapProtoFromLegacyFormat(lines):
+def GenerateCareMapProtoFromLegacyFormat(lines, fingerprint_enabled):
   """Constructs a care map proto message from the lines of the input file."""
 
   # Expected format of the legacy care_map.txt:
   # system
   # system's care_map ranges
+  # [system's fingerprint property id]
+  # [system's fingerprint]
   # [vendor]
   # [vendor's care_map ranges]
+  # [vendor's fingerprint property id]
+  # [vendor's fingerprint]
   # ...
-  assert len(lines) % 2 == 0, "line count must be even: {}".format(len(lines))
+
+  step = 4 if fingerprint_enabled else 2
+  assert len(lines) % step == 0, \
+      "line count must be multiple of {}: {}".format(step, len(lines))
 
   care_map_proto = care_map_pb2.CareMap()
-  for index in range(0, len(lines), 2):
+  for index in range(0, len(lines), step):
     info = care_map_proto.partitions.add()
     info.name = lines[index]
     info.ranges = lines[index + 1]
-
-    logging.info("Adding '%s': '%s' to care map", info.name, info.ranges)
+    if fingerprint_enabled:
+      info.id = lines[index + 2]
+      info.fingerprint = lines[index + 3]
+    logging.info("Care map info: name %s, ranges %s, id %s, fingerprint %s",
+                 info.name, info.ranges, info.id, info.fingerprint)
 
   return care_map_proto
 
 
-def ParseProtoMessage(message):
+def ParseProtoMessage(message, fingerprint_enabled):
   """Parses the care_map proto message and returns its text representation.
   Args:
-    message: care_map in protobuf message
+    message: Care_map in protobuf format.
+    fingerprint_enabled: Input protobuf message contains the fields 'id' and
+        'fingerprint'.
 
   Returns:
      A string of the care_map information, similar to the care_map legacy
@@ -66,8 +78,11 @@
     assert info.name, "partition name is required in care_map"
     assert info.ranges, "source range is required in care_map"
     info_list += [info.name, info.ranges]
+    if fingerprint_enabled:
+      assert info.id, "property id is required in care_map"
+      assert info.fingerprint, "fingerprint is required in care_map"
+      info_list += [info.id, info.fingerprint]
 
-    # TODO(xunchang) add a flag to output id & fingerprint also.
   return '\n'.join(info_list)
 
 
@@ -81,6 +96,10 @@
                            " specified).")
   parser.add_argument("output_file",
                       help="Path to output file to write the result.")
+  parser.add_argument("--no_fingerprint", action="store_false",
+                      dest="fingerprint_enabled",
+                      help="The 'id' and 'fingerprint' fields are disabled in"
+                           " the caremap.")
   parser.add_argument("--parse_proto", "-p", action="store_true",
                       help="Parses the input as proto message, and outputs"
                            " the care_map in plain text.")
@@ -96,10 +115,10 @@
     content = input_care_map.read()
 
   if args.parse_proto:
-    result = ParseProtoMessage(content)
+    result = ParseProtoMessage(content, args.fingerprint_enabled)
   else:
     care_map_proto = GenerateCareMapProtoFromLegacyFormat(
-        content.rstrip().splitlines())
+        content.rstrip().splitlines(), args.fingerprint_enabled)
     result = care_map_proto.SerializeToString()
 
   with open(args.output_file, 'w') as output:
diff --git a/update_verifier/include/update_verifier/update_verifier.h b/update_verifier/include/update_verifier/update_verifier.h
index 534384e..b00890e 100644
--- a/update_verifier/include/update_verifier/update_verifier.h
+++ b/update_verifier/include/update_verifier/update_verifier.h
@@ -16,17 +16,59 @@
 
 #pragma once
 
+#include <functional>
+#include <map>
 #include <string>
+#include <vector>
 
-int update_verifier(int argc, char** argv);
+#include "otautil/rangeset.h"
 
-// Returns true to indicate a passing verification (or the error should be ignored); Otherwise
-// returns false on fatal errors, where we should reject the current boot and trigger a fallback.
-// This function tries to process the care_map.txt as protobuf message; and falls back to use the
-// plain text format if the parse failed.
-//
+// The update verifier performs verification upon the first boot to a new slot on A/B devices.
+// During the verification, it reads all the blocks in the care_map. And if a failure happens,
+// it rejects the current boot and triggers a fallback.
+
 // Note that update_verifier should be backward compatible to not reject care_map.txt from old
 // releases, which could otherwise fail to boot into the new release. For example, we've changed
 // the care_map format between N and O. An O update_verifier would fail to work with N care_map.txt.
 // This could be a result of sideloading an O OTA while the device having a pending N update.
-bool verify_image(const std::string& care_map_name);
+int update_verifier(int argc, char** argv);
+
+// The UpdateVerifier parses the content in the care map, and continues to verify the
+// partitions by reading the cared blocks if there's no format error in the file. Otherwise,
+// it should skip the verification to avoid bricking the device.
+class UpdateVerifier {
+ public:
+  UpdateVerifier();
+
+  // This function tries to process the care_map.pb as protobuf message; and falls back to use
+  // care_map.txt if the pb format file doesn't exist. If the parsing succeeds, put the result
+  // of the pair <partition_name, ranges> into the |partition_map_|.
+  bool ParseCareMap();
+
+  // Verifies the new boot by reading all the cared blocks for partitions in |partition_map_|.
+  bool VerifyPartitions();
+
+ private:
+  friend class UpdateVerifierTest;
+  // Parses the legacy care_map.txt in plain text format.
+  bool ParseCareMapPlainText(const std::string& content);
+
+  // Finds all the dm-enabled partitions, and returns a map of <partition_name, block_device>.
+  std::map<std::string, std::string> FindDmPartitions();
+
+  // Returns true if we successfully read the blocks in |ranges| of the |dm_block_device|.
+  bool ReadBlocks(const std::string partition_name, const std::string& dm_block_device,
+                  const RangeSet& ranges);
+
+  // Functions to override the care_map_prefix_ and property_reader_, used in test only.
+  void set_care_map_prefix(const std::string& prefix);
+  void set_property_reader(const std::function<std::string(const std::string&)>& property_reader);
+
+  std::map<std::string, RangeSet> partition_map_;
+  // The path to the care_map excluding the filename extension; default value:
+  // "/data/ota_package/care_map"
+  std::string care_map_prefix_;
+
+  // The function to read the device property; default value: android::base::GetProperty()
+  std::function<std::string(const std::string&)> property_reader_;
+};
diff --git a/update_verifier/update_verifier.cpp b/update_verifier/update_verifier.cpp
index 5e5aa18..d7cd061 100644
--- a/update_verifier/update_verifier.cpp
+++ b/update_verifier/update_verifier.cpp
@@ -48,8 +48,6 @@
 
 #include <algorithm>
 #include <future>
-#include <string>
-#include <vector>
 
 #include <android-base/file.h>
 #include <android-base/logging.h>
@@ -61,13 +59,14 @@
 #include <cutils/android_reboot.h>
 
 #include "care_map.pb.h"
-#include "otautil/rangeset.h"
 
 using android::sp;
 using android::hardware::boot::V1_0::IBootControl;
 using android::hardware::boot::V1_0::BoolResult;
 using android::hardware::boot::V1_0::CommandResult;
 
+constexpr const char* kDefaultCareMapPrefix = "/data/ota_package/care_map";
+
 // Find directories in format of "/sys/block/dm-X".
 static int dm_name_filter(const dirent* de) {
   if (android::base::StartsWith(de->d_name, "dm-")) {
@@ -76,29 +75,29 @@
   return 0;
 }
 
-static bool read_blocks(const std::string& partition, const std::string& range_str) {
-  if (partition != "system" && partition != "vendor" && partition != "product") {
-    LOG(ERROR) << "Invalid partition name \"" << partition << "\"";
-    return false;
-  }
-  // Iterate the content of "/sys/block/dm-X/dm/name". If it matches one of "system", "vendor" or
-  // "product", then dm-X is a dm-wrapped device for that target. We will later read all the
-  // ("cared") blocks from "/dev/block/dm-X" to ensure the target partition's integrity.
+UpdateVerifier::UpdateVerifier()
+    : care_map_prefix_(kDefaultCareMapPrefix),
+      property_reader_([](const std::string& id) { return android::base::GetProperty(id, ""); }) {}
+
+// Iterate the content of "/sys/block/dm-X/dm/name" and find all the dm-wrapped block devices.
+// We will later read all the ("cared") blocks from "/dev/block/dm-X" to ensure the target
+// partition's integrity.
+std::map<std::string, std::string> UpdateVerifier::FindDmPartitions() {
   static constexpr auto DM_PATH_PREFIX = "/sys/block/";
   dirent** namelist;
   int n = scandir(DM_PATH_PREFIX, &namelist, dm_name_filter, alphasort);
   if (n == -1) {
     PLOG(ERROR) << "Failed to scan dir " << DM_PATH_PREFIX;
-    return false;
+    return {};
   }
   if (n == 0) {
-    LOG(ERROR) << "dm block device not found for " << partition;
-    return false;
+    LOG(ERROR) << "No dm block device found.";
+    return {};
   }
 
   static constexpr auto DM_PATH_SUFFIX = "/dm/name";
   static constexpr auto DEV_PATH = "/dev/block/";
-  std::string dm_block_device;
+  std::map<std::string, std::string> dm_block_devices;
   while (n--) {
     std::string path = DM_PATH_PREFIX + std::string(namelist[n]->d_name) + DM_PATH_SUFFIX;
     std::string content;
@@ -110,33 +109,18 @@
       if (dm_block_name == "vroot") {
         dm_block_name = "system";
       }
-      if (dm_block_name == partition) {
-        dm_block_device = DEV_PATH + std::string(namelist[n]->d_name);
-        while (n--) {
-          free(namelist[n]);
-        }
-        break;
-      }
+
+      dm_block_devices.emplace(dm_block_name, DEV_PATH + std::string(namelist[n]->d_name));
     }
     free(namelist[n]);
   }
   free(namelist);
 
-  if (dm_block_device.empty()) {
-    LOG(ERROR) << "Failed to find dm block device for " << partition;
-    return false;
-  }
+  return dm_block_devices;
+}
 
-  // For block range string, first integer 'count' equals 2 * total number of valid ranges,
-  // followed by 'count' number comma separated integers. Every two integers reprensent a
-  // block range with the first number included in range but second number not included.
-  // For example '4,64536,65343,74149,74150' represents: [64536,65343) and [74149,74150).
-  RangeSet ranges = RangeSet::Parse(range_str);
-  if (!ranges) {
-    LOG(ERROR) << "Error parsing RangeSet string " << range_str;
-    return false;
-  }
-
+bool UpdateVerifier::ReadBlocks(const std::string partition_name,
+                                const std::string& dm_block_device, const RangeSet& ranges) {
   // RangeSet::Split() splits the ranges into multiple groups with same number of blocks (except for
   // the last group).
   size_t thread_num = std::thread::hardware_concurrency() ?: 4;
@@ -144,10 +128,10 @@
 
   std::vector<std::future<bool>> threads;
   for (const auto& group : groups) {
-    auto thread_func = [&group, &dm_block_device, &partition]() {
+    auto thread_func = [&group, &dm_block_device, &partition_name]() {
       android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(dm_block_device.c_str(), O_RDONLY)));
       if (fd.get() == -1) {
-        PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition;
+        PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition_name;
         return false;
       }
 
@@ -155,9 +139,7 @@
       std::vector<uint8_t> buf(1024 * kBlockSize);
 
       size_t block_count = 0;
-      for (const auto& range : group) {
-        size_t range_start = range.first;
-        size_t range_end = range.second;
+      for (const auto& [range_start, range_end] : group) {
         if (lseek64(fd.get(), static_cast<off64_t>(range_start) * kBlockSize, SEEK_SET) == -1) {
           PLOG(ERROR) << "lseek to " << range_start << " failed";
           return false;
@@ -190,26 +172,20 @@
   return ret;
 }
 
-static bool process_care_map_plain_text(const std::string& care_map_contents) {
-  // care_map file has up to six lines, where every two lines make a pair. Within each pair, the
-  // first line has the partition name (e.g. "system"), while the second line holds the ranges of
-  // all the blocks to verify.
-  std::vector<std::string> lines =
-      android::base::Split(android::base::Trim(care_map_contents), "\n");
-  if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
-    LOG(ERROR) << "Invalid lines in care_map: found " << lines.size()
-               << " lines, expecting 2 or 4 or 6 lines.";
+bool UpdateVerifier::VerifyPartitions() {
+  auto dm_block_devices = FindDmPartitions();
+  if (dm_block_devices.empty()) {
+    LOG(ERROR) << "No dm-enabled block device is found.";
     return false;
   }
 
-  for (size_t i = 0; i < lines.size(); i += 2) {
-    // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
-    // update_verifier (the last few metadata blocks can't be read via device mapper).
-    if (android::base::StartsWith(lines[i], "/dev/block/")) {
-      LOG(WARNING) << "Found legacy care_map.txt; skipped.";
-      return true;
+  for (const auto& [partition_name, ranges] : partition_map_) {
+    if (dm_block_devices.find(partition_name) == dm_block_devices.end()) {
+      LOG(ERROR) << "Failed to find dm block device for " << partition_name;
+      return false;
     }
-    if (!read_blocks(lines[i], lines[i+1])) {
+
+    if (!ReadBlocks(partition_name, dm_block_devices.at(partition_name), ranges)) {
       return false;
     }
   }
@@ -217,50 +193,133 @@
   return true;
 }
 
-bool verify_image(const std::string& care_map_name) {
+bool UpdateVerifier::ParseCareMapPlainText(const std::string& content) {
+  // care_map file has up to six lines, where every two lines make a pair. Within each pair, the
+  // first line has the partition name (e.g. "system"), while the second line holds the ranges of
+  // all the blocks to verify.
+  auto lines = android::base::Split(android::base::Trim(content), "\n");
+  if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
+    LOG(WARNING) << "Invalid lines in care_map: found " << lines.size()
+                 << " lines, expecting 2 or 4 or 6 lines.";
+    return false;
+  }
+
+  for (size_t i = 0; i < lines.size(); i += 2) {
+    const std::string& partition_name = lines[i];
+    const std::string& range_str = lines[i + 1];
+    // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
+    // update_verifier (the last few metadata blocks can't be read via device mapper).
+    if (android::base::StartsWith(partition_name, "/dev/block/")) {
+      LOG(WARNING) << "Found legacy care_map.txt; skipped.";
+      return false;
+    }
+
+    // For block range string, first integer 'count' equals 2 * total number of valid ranges,
+    // followed by 'count' number comma separated integers. Every two integers reprensent a
+    // block range with the first number included in range but second number not included.
+    // For example '4,64536,65343,74149,74150' represents: [64536,65343) and [74149,74150).
+    RangeSet ranges = RangeSet::Parse(range_str);
+    if (!ranges) {
+      LOG(WARNING) << "Error parsing RangeSet string " << range_str;
+      return false;
+    }
+
+    partition_map_.emplace(partition_name, ranges);
+  }
+
+  return true;
+}
+
+bool UpdateVerifier::ParseCareMap() {
+  partition_map_.clear();
+
+  std::string care_map_name = care_map_prefix_ + ".pb";
+  if (access(care_map_name.c_str(), R_OK) == -1) {
+    LOG(WARNING) << care_map_name
+                 << " doesn't exist, falling back to read the care_map in plain text format.";
+    care_map_name = care_map_prefix_ + ".txt";
+  }
+
   android::base::unique_fd care_map_fd(TEMP_FAILURE_RETRY(open(care_map_name.c_str(), O_RDONLY)));
   // If the device is flashed before the current boot, it may not have care_map.txt in
   // /data/ota_package. To allow the device to continue booting in this situation, we should
   // print a warning and skip the block verification.
   if (care_map_fd.get() == -1) {
     PLOG(WARNING) << "Failed to open " << care_map_name;
-    return true;
+    return false;
   }
 
   std::string file_content;
   if (!android::base::ReadFdToString(care_map_fd.get(), &file_content)) {
-    PLOG(ERROR) << "Failed to read " << care_map_name;
+    PLOG(WARNING) << "Failed to read " << care_map_name;
     return false;
   }
 
   if (file_content.empty()) {
-    LOG(ERROR) << "Unexpected empty care map";
+    LOG(WARNING) << "Unexpected empty care map";
     return false;
   }
 
-  UpdateVerifier::CareMap care_map;
-  // Falls back to use the plain text version if we cannot parse the file as protobuf message.
+  if (android::base::EndsWith(care_map_name, ".txt")) {
+    return ParseCareMapPlainText(file_content);
+  }
+
+  recovery_update_verifier::CareMap care_map;
   if (!care_map.ParseFromString(file_content)) {
-    return process_care_map_plain_text(file_content);
+    LOG(WARNING) << "Failed to parse " << care_map_name << " in protobuf format.";
+    return false;
   }
 
   for (const auto& partition : care_map.partitions()) {
     if (partition.name().empty()) {
-      LOG(ERROR) << "Unexpected empty partition name.";
+      LOG(WARNING) << "Unexpected empty partition name.";
       return false;
     }
     if (partition.ranges().empty()) {
-      LOG(ERROR) << "Unexpected block ranges for partition " << partition.name();
+      LOG(WARNING) << "Unexpected block ranges for partition " << partition.name();
       return false;
     }
-    if (!read_blocks(partition.name(), partition.ranges())) {
+    RangeSet ranges = RangeSet::Parse(partition.ranges());
+    if (!ranges) {
+      LOG(WARNING) << "Error parsing RangeSet string " << partition.ranges();
       return false;
     }
+
+    // Continues to check other partitions if there is a fingerprint mismatch.
+    if (partition.id().empty() || partition.id() == "unknown") {
+      LOG(WARNING) << "Skip reading partition " << partition.name()
+                   << ": property_id is not provided to get fingerprint.";
+      continue;
+    }
+
+    std::string fingerprint = property_reader_(partition.id());
+    if (fingerprint != partition.fingerprint()) {
+      LOG(WARNING) << "Skip reading partition " << partition.name() << ": fingerprint "
+                   << fingerprint << " doesn't match the expected value "
+                   << partition.fingerprint();
+      continue;
+    }
+
+    partition_map_.emplace(partition.name(), ranges);
+  }
+
+  if (partition_map_.empty()) {
+    LOG(WARNING) << "No partition to verify";
+    return false;
   }
 
   return true;
 }
 
+void UpdateVerifier::set_care_map_prefix(const std::string& prefix) {
+  care_map_prefix_ = prefix;
+}
+
+void UpdateVerifier::set_property_reader(
+    const std::function<std::string(const std::string&)>& property_reader) {
+  property_reader_ = property_reader;
+}
+
 static int reboot_device() {
   if (android_reboot(ANDROID_RB_RESTART2, 0, nullptr) == -1) {
     LOG(ERROR) << "Failed to reboot.";
@@ -308,8 +367,10 @@
     }
 
     if (!skip_verification) {
-      static constexpr auto CARE_MAP_FILE = "/data/ota_package/care_map.txt";
-      if (!verify_image(CARE_MAP_FILE)) {
+      UpdateVerifier verifier;
+      if (!verifier.ParseCareMap()) {
+        LOG(WARNING) << "Failed to parse the care map file, skipping verification";
+      } else if (!verifier.VerifyPartitions()) {
         LOG(ERROR) << "Failed to verify all blocks in care map file.";
         return reboot_device();
       }
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index 8388456..47849a1 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -109,7 +109,7 @@
     return false;
   }
 
-  if (!android::base::ParseInt(lines[0], last_command_index)) {
+  if (!android::base::ParseUint(lines[0], last_command_index)) {
     LOG(ERROR) << "Failed to parse integer in: " << lines[0];
     return false;
   }
diff --git a/verifier.cpp b/verifier.cpp
index 283e043..1dc52a0 100644
--- a/verifier.cpp
+++ b/verifier.cpp
@@ -27,9 +27,13 @@
 #include <vector>
 
 #include <android-base/logging.h>
+#include <openssl/bio.h>
 #include <openssl/bn.h>
 #include <openssl/ecdsa.h>
+#include <openssl/evp.h>
 #include <openssl/obj_mac.h>
+#include <openssl/pem.h>
+#include <openssl/rsa.h>
 
 #include "asn1_decoder.h"
 #include "otautil/print_sha1.h"
@@ -441,6 +445,70 @@
     return key;
 }
 
+bool LoadCertificateFromBuffer(const std::vector<uint8_t>& pem_content, Certificate* cert) {
+  std::unique_ptr<BIO, decltype(&BIO_free)> content(
+      BIO_new_mem_buf(pem_content.data(), pem_content.size()), BIO_free);
+
+  std::unique_ptr<X509, decltype(&X509_free)> x509(
+      PEM_read_bio_X509(content.get(), nullptr, nullptr, nullptr), X509_free);
+  if (!x509) {
+    LOG(ERROR) << "Failed to read x509 certificate";
+    return false;
+  }
+
+  int nid = X509_get_signature_nid(x509.get());
+  switch (nid) {
+    // SignApk has historically accepted md5WithRSA certificates, but treated them as
+    // sha1WithRSA anyway. Continue to do so for backwards compatibility.
+    case NID_md5WithRSA:
+    case NID_md5WithRSAEncryption:
+    case NID_sha1WithRSA:
+    case NID_sha1WithRSAEncryption:
+      cert->hash_len = SHA_DIGEST_LENGTH;
+      break;
+    case NID_sha256WithRSAEncryption:
+    case NID_ecdsa_with_SHA256:
+      cert->hash_len = SHA256_DIGEST_LENGTH;
+      break;
+    default:
+      LOG(ERROR) << "Unrecognized signature nid " << OBJ_nid2ln(nid);
+      return false;
+  }
+
+  std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> public_key(X509_get_pubkey(x509.get()),
+                                                                 EVP_PKEY_free);
+  if (!public_key) {
+    LOG(ERROR) << "Failed to extract the public key from x509 certificate";
+    return false;
+  }
+
+  int key_type = EVP_PKEY_id(public_key.get());
+  // 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";
+      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";
+      return false;
+    }
+  } else {
+    LOG(ERROR) << "Unrecognized public key type " << OBJ_nid2ln(key_type);
+    return false;
+  }
+
+  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:
diff --git a/verifier.h b/verifier.h
index 6fa8f2b..b134241 100644
--- a/verifier.h
+++ b/verifier.h
@@ -17,6 +17,8 @@
 #ifndef _RECOVERY_VERIFIER_H
 #define _RECOVERY_VERIFIER_H
 
+#include <stdint.h>
+
 #include <functional>
 #include <memory>
 #include <vector>
@@ -70,6 +72,10 @@
 
 bool load_keys(const char* filename, std::vector<Certificate>& certs);
 
+// Parses a PEM-encoded x509 certificate from the given buffer and saves it into |cert|. Returns
+// false if there is a parsing failure or the signature's encryption algorithm is not supported.
+bool LoadCertificateFromBuffer(const std::vector<uint8_t>& pem_content, Certificate* cert);
+
 #define VERIFY_SUCCESS        0
 #define VERIFY_FAILURE        1
 
diff --git a/wear_ui.cpp b/wear_ui.cpp
index 3b057b7..8f3bc7b 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -73,7 +73,7 @@
   if (!show_text) {
     draw_foreground_locked();
   } else {
-    SetColor(TEXT_FILL);
+    SetColor(UIElement::TEXT_FILL);
     gr_fill(0, 0, gr_fb_width(), gr_fb_height());
 
     // clang-format off
@@ -99,8 +99,9 @@
                                const std::vector<std::string>& items, size_t initial_selection) {
   std::lock_guard<std::mutex> lg(updateMutex);
   if (text_rows_ > 0 && text_cols_ > 0) {
-    menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1,
-                                   text_cols_ - 1, headers, items, initial_selection);
+    menu_ = std::make_unique<TextMenu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1,
+                                       text_cols_ - 1, headers, items, initial_selection,
+                                       char_height_, *this);
     update_screen_locked();
   }
 }