summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Android.bp1
-rw-r--r--Android.mk9
-rw-r--r--fsck_unshare_blocks.cpp7
-rw-r--r--install.h11
-rw-r--r--logging.cpp1
-rw-r--r--minadbd/Android.bp2
-rw-r--r--minadbd/minadbd_services.cpp21
-rw-r--r--otautil/Android.bp1
-rw-r--r--otautil/include/otautil/parse_install_logs.h33
-rw-r--r--otautil/parse_install_logs.cpp114
-rw-r--r--recovery-persist.cpp56
-rw-r--r--recovery.cpp26
-rw-r--r--recovery_main.cpp7
-rw-r--r--screen_ui.cpp246
-rw-r--r--screen_ui.h167
-rw-r--r--tests/Android.bp14
-rw-r--r--tests/component/update_verifier_test.cpp143
-rw-r--r--tests/component/verifier_test.cpp84
-rw-r--r--tests/unit/parse_install_logs_test.cpp75
-rw-r--r--tests/unit/screen_ui_test.cpp59
-rw-r--r--tools/image_generator/Android.bp23
-rw-r--r--tools/image_generator/ImageGenerator.java394
-rw-r--r--tools/image_generator/ImageGenerator.mf1
-rw-r--r--tools/image_generator/README.md20
-rw-r--r--update_verifier/Android.bp5
-rw-r--r--update_verifier/care_map.proto2
-rw-r--r--update_verifier/care_map_generator.py39
-rw-r--r--update_verifier/include/update_verifier/update_verifier.h56
-rw-r--r--update_verifier/update_verifier.cpp183
-rw-r--r--updater/blockimg.cpp2
-rw-r--r--verifier.cpp68
-rw-r--r--verifier.h6
-rw-r--r--wear_ui.cpp7
33 files changed, 1595 insertions, 288 deletions
diff --git a/Android.bp b/Android.bp
index afab76c20..76e69859c 100644
--- a/Android.bp
+++ b/Android.bp
@@ -256,6 +256,7 @@ cc_binary {
shared_libs: [
"libbase",
"liblog",
+ "libmetricslogger",
],
static_libs: [
diff --git a/Android.mk b/Android.mk
index 80d107dc4..7be123069 100644
--- a/Android.mk
+++ b/Android.mk
@@ -71,10 +71,13 @@ LOCAL_REQUIRED_MODULES += \
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 2e6b5b807..684958e38 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 @@ bool do_fsck_unshare_blocks() {
// 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 0f6670a35..1d3d0cd27 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 d5af72aad..50642a26b 100644
--- a/logging.cpp
+++ b/logging.cpp
@@ -221,6 +221,7 @@ void copy_logs(bool modified_flash, bool has_cache) {
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 00244ee7e..370232b3f 100644
--- a/minadbd/Android.bp
+++ b/minadbd/Android.bp
@@ -52,6 +52,7 @@ cc_library {
cc_test {
name: "minadbd_test",
+ isolated: true,
defaults: [
"minadbd_defaults",
@@ -64,7 +65,6 @@ cc_test {
static_libs: [
"libminadbd_services",
"libadbd",
- "libBionicGtestMain",
],
shared_libs: [
diff --git a/minadbd/minadbd_services.cpp b/minadbd/minadbd_services.cpp
index ab1939e92..e9c51da0a 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 56c7c9e89..41018dd2f 100644
--- a/otautil/Android.bp
+++ b/otautil/Android.bp
@@ -41,6 +41,7 @@ cc_library_static {
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 000000000..135d29ccf
--- /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 000000000..13a729921
--- /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 d3ade6260..ebb42d223 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 @@ ssize_t logsave(
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 @@ int main(int argc, char **argv) {
}
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 @@ int main(int argc, char **argv) {
__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 5934b61d7..3ea282fc0 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -78,6 +78,7 @@ static constexpr const char* CACHE_ROOT = "/cache";
static constexpr const char* DATA_ROOT = "/data";
static constexpr const char* METADATA_ROOT = "/metadata";
static constexpr const char* SDCARD_ROOT = "/sdcard";
+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 @@ static bool wipe_data(Device* device) {
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 @@ static bool prompt_and_wipe_data(Device* device) {
// 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 @@ static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
}
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 @@ Device::BuiltinAction start_recovery(Device* device, const std::vector<std::stri
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 c3168fc23..78350944c 100644
--- a/recovery_main.cpp
+++ b/recovery_main.cpp
@@ -364,7 +364,8 @@ int main(int argc, char** argv) {
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 @@ int main(int argc, char** argv) {
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 391dedb00..181d58ecf 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -54,15 +54,23 @@ static double now() {
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 @@ Menu::Menu(bool scrollable, size_t max_items, size_t max_length,
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 @@ bool Menu::ItemsOverflow(std::string* cur_selection_str) const {
}
// 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 @@ int Menu::Select(int sel) {
return selection_;
}
+int TextMenu::DrawHeader(int x, int y) const {
+ int offset = 0;
+
+ draw_funcs_.SetColor(UIElement::HEADER);
+ if (!scrollable()) {
+ offset += draw_funcs_.DrawWrappedTextLines(x, y + offset, text_headers());
+ } else {
+ offset += draw_funcs_.DrawTextLines(x, y + offset, text_headers());
+ // Show the current menu item number in relation to total number if items don't fit on the
+ // screen.
+ std::string cur_selection_str;
+ if (ItemsOverflow(&cur_selection_str)) {
+ offset += draw_funcs_.DrawTextLine(x, y + offset, cur_selection_str, true);
+ }
+ }
+
+ return offset;
+}
+
+int TextMenu::DrawItems(int x, int y, int screen_width, bool long_press) const {
+ int offset = 0;
+
+ draw_funcs_.SetColor(UIElement::MENU);
+ // Do not draw the horizontal rule for wear devices.
+ if (!scrollable()) {
+ offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4;
+ }
+ for (size_t i = MenuStart(); i < MenuEnd(); ++i) {
+ bool bold = false;
+ if (i == selection()) {
+ // Draw the highlight bar.
+ draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG);
+
+ int bar_height = char_height_ + 4;
+ draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height);
+
+ // Bold white text for the selected item.
+ draw_funcs_.SetColor(UIElement::MENU_SEL_FG);
+ bold = true;
+ }
+ offset += draw_funcs_.DrawTextLine(x, y + offset, TextItem(i), bold);
+
+ draw_funcs_.SetColor(UIElement::MENU);
+ }
+ offset += draw_funcs_.DrawHorizontalRule(y + offset);
+
+ return offset;
+}
+
+GraphicMenu::GraphicMenu(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::draw_foreground_locked() {
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 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string
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 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string
// 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 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(
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 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(
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 @@ void ScreenRecoveryUI::StartMenu(const std::vector<std::string>& headers,
const std::vector<std::string>& items, size_t initial_selection) {
std::lock_guard<std::mutex> lg(updateMutex);
if (text_rows_ > 0 && text_cols_ > 1) {
- menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
- initial_selection);
+ 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 f08f4f4f3..915288793 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 @@ class Menu {
// |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 @@ class Menu {
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 @@ class ScreenRecoveryUI : public RecoveryUI {
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 @@ class ScreenRecoveryUI : public RecoveryUI {
// 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 ab4d31da2..2cfc32572 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -45,7 +45,7 @@ cc_defaults {
static_libs: [
"libutils",
],
- }
+ },
},
}
@@ -93,12 +93,14 @@ librecovery_static_libs = [
"libhidlbase",
"libhidltransport",
"libhwbinder",
+ "libbinderthreadstate",
"libvndksupport",
"libtinyxml2",
]
cc_test {
name: "recovery_unit_test",
+ isolated: true,
defaults: [
"recovery_test_defaults",
@@ -117,7 +119,6 @@ cc_test {
"libotautil",
"libupdater",
"libgtest_prod",
- "libBionicGtestMain",
],
data: ["testdata/*"],
@@ -125,6 +126,7 @@ cc_test {
cc_test {
name: "recovery_manual_test",
+ isolated: true,
defaults: [
"recovery_test_defaults",
@@ -135,14 +137,11 @@ cc_test {
srcs: [
"manual/recovery_test.cpp",
],
-
- static_libs: [
- "libBionicGtestMain",
- ],
}
cc_test {
name: "recovery_component_test",
+ isolated: true,
defaults: [
"recovery_test_defaults",
@@ -159,7 +158,6 @@ cc_test {
"libupdater",
"libupdate_verifier",
"libprotobuf-cpp-lite",
- "libBionicGtestMain",
],
data: [
@@ -170,6 +168,7 @@ cc_test {
cc_test_host {
name: "recovery_host_test",
+ isolated: true,
defaults: [
"recovery_test_defaults",
@@ -193,7 +192,6 @@ cc_test_host {
"libdivsufsort64",
"libdivsufsort",
"libz",
- "libBionicGtestMain",
],
data: ["testdata/*"],
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index a97071635..2420c27fe 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 @@ class UpdateVerifierTest : public ::testing::Test {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_smoke) {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) {
}
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 @@ TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) {
}
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_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_file.path));
- ASSERT_FALSE(verify_image(care_map_file.path));
+ 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 3246ecdbc..c460cbe6f 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 000000000..8061f3be1
--- /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 7d97a006b..ec269503e 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 @@ TEST(ScreenUITest, StartPhoneMenuSmoke) {
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 @@ TEST(ScreenUITest, StartWearMenuSmoke) {
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 @@ TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
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 @@ TEST(ScreenUITest, StartWearMenuItemsOverflow) {
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, PhoneMenuSelectSmoke) {
}
}
-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, WearMenuSelectSmoke) {
}
}
-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 000000000..3f718fec5
--- /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 000000000..f2262166a
--- /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 000000000..17712d129
--- /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 000000000..22e32f6ce
--- /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 7a860a149..1b84619af 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 442ddd4a9..15d3afa83 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 5057ffea7..051d98deb 100644
--- a/update_verifier/care_map_generator.py
+++ b/update_verifier/care_map_generator.py
@@ -27,32 +27,44 @@ import sys
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 @@ def ParseProtoMessage(message):
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 @@ def main(argv):
" 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 @@ def main(argv):
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 534384e1d..b00890e82 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"
+
+// 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.
-// 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.
-//
// 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 5e5aa1819..d7cd061e2 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 @@ static int dm_name_filter(const dirent* de) {
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 @@ static bool read_blocks(const std::string& partition, const std::string& range_s
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;
- }
-
- // 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;
- }
+ return dm_block_devices;
+}
+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 @@ static bool read_blocks(const std::string& partition, const std::string& range_s
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 @@ static bool read_blocks(const std::string& partition, const std::string& range_s
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,77 +172,154 @@ static bool read_blocks(const std::string& partition, const std::string& range_s
return ret;
}
-static bool process_care_map_plain_text(const std::string& care_map_contents) {
+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 (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 (!ReadBlocks(partition_name, dm_block_devices.at(partition_name), ranges)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool UpdateVerifier::ParseCareMapPlainText(const std::string& content) {
// care_map file has up to six lines, where every two lines make a pair. Within each pair, the
// first line has the partition name (e.g. "system"), while the second line holds the ranges of
// all the blocks to verify.
- std::vector<std::string> lines =
- android::base::Split(android::base::Trim(care_map_contents), "\n");
+ auto lines = android::base::Split(android::base::Trim(content), "\n");
if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
- LOG(ERROR) << "Invalid lines in care_map: found " << lines.size()
- << " lines, expecting 2 or 4 or 6 lines.";
+ 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(lines[i], "/dev/block/")) {
+ if (android::base::StartsWith(partition_name, "/dev/block/")) {
LOG(WARNING) << "Found legacy care_map.txt; skipped.";
- return true;
+ return false;
}
- if (!read_blocks(lines[i], lines[i+1])) {
+
+ // 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 verify_image(const std::string& care_map_name) {
+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 @@ int update_verifier(int argc, char** argv) {
}
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 838845673..47849a155 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -109,7 +109,7 @@ static bool ParseLastCommandFile(size_t* last_command_index) {
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 283e04300..1dc52a0ef 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 @@ std::unique_ptr<EC_KEY, ECKEYDeleter> parse_ec_key(FILE* file) {
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 6fa8f2b0a..b13424126 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 @@ int verify_file(const unsigned char* addr, size_t length, const std::vector<Cert
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 3b057b761..8f3bc7bbe 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -73,7 +73,7 @@ void WearRecoveryUI::draw_screen_locked() {
if (!show_text) {
draw_foreground_locked();
} else {
- SetColor(TEXT_FILL);
+ SetColor(UIElement::TEXT_FILL);
gr_fill(0, 0, gr_fb_width(), gr_fb_height());
// clang-format off
@@ -99,8 +99,9 @@ void WearRecoveryUI::StartMenu(const std::vector<std::string>& headers,
const std::vector<std::string>& items, size_t initial_selection) {
std::lock_guard<std::mutex> lg(updateMutex);
if (text_rows_ > 0 && text_cols_ > 0) {
- menu_ = 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();
}
}