diff options
187 files changed, 5712 insertions, 2718 deletions
diff --git a/Android.bp b/Android.bp index afab76c20..10f6c7976 100644 --- a/Android.bp +++ b/Android.bp @@ -24,8 +24,6 @@ cc_defaults { "-Wall", "-Werror", ], - - cpp_std: "c++17", } cc_library { @@ -47,6 +45,7 @@ cc_library { static_libs: [ "libminui", "libotautil", + "libfstab", ], shared_libs: [ @@ -159,6 +158,7 @@ cc_defaults { "libhealthhalutils", "libvintf_recovery", "libvintf", + "libfstab", ], } @@ -175,6 +175,7 @@ cc_library_static { "fsck_unshare_blocks.cpp", "fuse_sdcard_provider.cpp", "install.cpp", + "package.cpp", "recovery.cpp", "roots.cpp", ], @@ -201,6 +202,7 @@ cc_library_static { "libbase", "libcrypto", "libcrypto_utils", + "libziparchive", ], static_libs: [ @@ -256,10 +258,12 @@ cc_binary { shared_libs: [ "libbase", "liblog", + "libmetricslogger", ], static_libs: [ "libotautil", + "libfstab", ], init_rc: [ @@ -287,6 +291,7 @@ cc_binary { static_libs: [ "libotautil", + "libfstab", ], init_rc: [ diff --git a/Android.mk b/Android.mk index 80d107dc4..9806d1091 100644 --- a/Android.mk +++ b/Android.mk @@ -58,23 +58,18 @@ LOCAL_MODULE := recovery_deps ifeq ($(TARGET_USERIMAGES_USE_F2FS),true) ifeq ($(HOST_OS),linux) LOCAL_REQUIRED_MODULES += \ - sload.f2fs \ - mkfs.f2fs -endif -endif - -# e2fsck is needed for adb remount -R. -ifeq ($(BOARD_EXT4_SHARE_DUP_BLOCKS),true) -ifneq (,$(filter userdebug eng,$(TARGET_BUILD_VARIANT))) -LOCAL_REQUIRED_MODULES += \ - e2fsck_static + make_f2fs.recovery \ + sload_f2fs.recovery 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/CleanSpec.mk b/CleanSpec.mk index e2d97d42b..fec823e7e 100644 --- a/CleanSpec.mk +++ b/CleanSpec.mk @@ -49,3 +49,4 @@ # ************************************************ $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/EXECUTABLES/recovery_intermediates) $(call add-clean-step, rm -rf $(PRODUCT_OUT)/obj/STATIC_LIBRARIES/libminui_intermediates/import_includes) +$(call add-clean-step, rm -rf $(PRODUCT_OUT)/recovery/root/sbin) diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index 108429193..28aa06f45 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -7,5 +7,4 @@ clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} - -fw updater_sample/ - + --file_whitelist tools/ updater_sample/ @@ -41,13 +41,6 @@ Running the manual tests contents of pmsg buffer into /data/misc/recovery/inject.txt. Test will pass if this file has expected contents. -`ResourceTest` validates whether the png files are qualified as background text -image under recovery. - - 1. `adb sync data` to make sure the test-dir has the images to test. - 2. The test will automatically pickup and verify all `_text.png` files in - the test dir. - Using `adb` under recovery -------------------------- @@ -60,10 +53,10 @@ allows `adb` communication. A device should be listed under `adb devices`, eithe List of devices attached 1234567890abcdef recovery -Although `/sbin/adbd` shares the same binary between normal boot and recovery images, only a subset -of `adb` commands are meaningful under recovery, such as `adb root`, `adb shell`, `adb push`, `adb -pull` etc. `adb shell` works only after manually mounting `/system` from recovery menu (assuming a -valid system image on device). +Although `/system/bin/adbd` is built from the same code base as the one in the normal boot, only a +subset of `adb` commands are meaningful under recovery, such as `adb root`, `adb shell`, `adb push`, +`adb pull` etc. Since Android Q, `adb shell` no longer requires manually mounting `/system` from +recovery menu. ## Troubleshooting @@ -74,8 +67,8 @@ valid system image on device). * Ensure `adbd` is built and running. -By default, `adbd` is always included into recovery image, as `/sbin/adbd`. `init` starts `adbd` -service automatically only in debuggable builds. This behavior is controlled by the recovery +By default, `adbd` is always included into recovery image, as `/system/bin/adbd`. `init` starts +`adbd` service automatically only in debuggable builds. This behavior is controlled by the recovery specific `/init.rc`, whose source code is at `bootable/recovery/etc/init.rc`. The best way to confirm a running `adbd` is by checking the serial output, which shows a service diff --git a/applypatch/freecache.cpp b/applypatch/freecache.cpp index e4878655e..3868ef230 100644 --- a/applypatch/freecache.cpp +++ b/applypatch/freecache.cpp @@ -141,8 +141,9 @@ static int64_t FreeSpaceForFile(const std::string& filename) { return -1; } - int64_t free_space = static_cast<int64_t>(sf.f_bsize) * sf.f_bavail; - if (sf.f_bsize == 0 || free_space / sf.f_bsize != sf.f_bavail) { + auto f_bsize = static_cast<int64_t>(sf.f_bsize); + auto free_space = sf.f_bsize * sf.f_bavail; + if (f_bsize == 0 || free_space / f_bsize != static_cast<int64_t>(sf.f_bavail)) { LOG(ERROR) << "Invalid block size or overflow (sf.f_bsize " << sf.f_bsize << ", sf.f_bavail " << sf.f_bavail << ")"; return -1; @@ -170,6 +171,13 @@ bool CheckAndFreeSpaceOnCache(size_t bytes) { bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname, const std::function<int64_t(const std::string&)>& space_checker) { + // The requested size cannot exceed max int64_t. + if (static_cast<uint64_t>(bytes_needed) > + static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) { + LOG(ERROR) << "Invalid arg of bytes_needed: " << bytes_needed; + return false; + } + struct stat st; if (stat(dirname.c_str(), &st) == -1) { PLOG(ERROR) << "Failed to stat " << dirname; @@ -187,7 +195,7 @@ bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname, } LOG(INFO) << free_now << " bytes free on " << dirname << " (" << bytes_needed << " needed)"; - if (free_now >= bytes_needed) { + if (free_now >= static_cast<int64_t>(bytes_needed)) { return true; } @@ -230,7 +238,7 @@ bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname, return false; } LOG(INFO) << "Deleted " << file << "; now " << free_now << " bytes free"; - if (free_now >= bytes_needed) { + if (free_now >= static_cast<int64_t>(bytes_needed)) { return true; } } diff --git a/applypatch/imgpatch.cpp b/applypatch/imgpatch.cpp index e6be39a2f..f4c33e5a3 100644 --- a/applypatch/imgpatch.cpp +++ b/applypatch/imgpatch.cpp @@ -54,7 +54,7 @@ static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_ const Value& patch, size_t patch_offset, const char* deflate_header, SinkFn sink) { size_t expected_target_length = static_cast<size_t>(Read8(deflate_header + 32)); - CHECK_GT(expected_target_length, 0); + CHECK_GT(expected_target_length, static_cast<size_t>(0)); int level = Read4(deflate_header + 40); int method = Read4(deflate_header + 44); int window_bits = Read4(deflate_header + 48); diff --git a/bootloader_message/bootloader_message.cpp b/bootloader_message/bootloader_message.cpp index aaeffdc5c..8c1d63bdd 100644 --- a/bootloader_message/bootloader_message.cpp +++ b/bootloader_message/bootloader_message.cpp @@ -27,21 +27,25 @@ #include <android-base/properties.h> #include <android-base/stringprintf.h> #include <android-base/unique_fd.h> -#include <fs_mgr.h> +#include <fstab/fstab.h> + +using android::fs_mgr::Fstab; +using android::fs_mgr::ReadDefaultFstab; static std::string get_misc_blk_device(std::string* err) { - std::unique_ptr<fstab, decltype(&fs_mgr_free_fstab)> fstab(fs_mgr_read_fstab_default(), - fs_mgr_free_fstab); - if (!fstab) { + Fstab fstab; + if (!ReadDefaultFstab(&fstab)) { *err = "failed to read default fstab"; return ""; } - fstab_rec* record = fs_mgr_get_entry_for_mount_point(fstab.get(), "/misc"); - if (record == nullptr) { - *err = "failed to find /misc partition"; - return ""; + for (const auto& entry : fstab) { + if (entry.mount_point == "/misc") { + return entry.blk_device; + } } - return record->blk_device; + + *err = "failed to find /misc partition"; + return ""; } // In recovery mode, recovery can get started and try to access the misc diff --git a/fsck_unshare_blocks.cpp b/fsck_unshare_blocks.cpp index 2e6b5b807..ce6940a6b 100644 --- a/fsck_unshare_blocks.cpp +++ b/fsck_unshare_blocks.cpp @@ -120,14 +120,10 @@ bool do_fsck_unshare_blocks() { std::vector<std::string> partitions = { "/odm", "/oem", "/product", "/vendor" }; // Temporarily mount system so we can copy e2fsck_static. - bool mounted = false; - if (android::base::GetBoolProperty("ro.build.system_root_image", false)) { - mounted = ensure_path_mounted_at("/", "/mnt/system") != -1; - partitions.push_back("/"); - } else { - mounted = ensure_path_mounted_at("/system", "/mnt/system") != -1; - partitions.push_back("/system"); - } + std::string system_root = get_system_root(); + bool mounted = ensure_path_mounted_at(system_root, "/mnt/system") != -1; + partitions.push_back(system_root); + if (!mounted) { LOG(ERROR) << "Failed to mount system image."; return false; diff --git a/install.cpp b/install.cpp index e379ef307..05f9af7a4 100644 --- a/install.cpp +++ b/install.cpp @@ -32,9 +32,7 @@ #include <condition_variable> #include <functional> #include <limits> -#include <map> #include <mutex> -#include <string> #include <thread> #include <vector> @@ -47,13 +45,13 @@ #include <android-base/strings.h> #include <android-base/unique_fd.h> #include <vintf/VintfObjectRecovery.h> -#include <ziparchive/zip_archive.h> #include "common.h" #include "otautil/error_code.h" #include "otautil/paths.h" #include "otautil/sysutil.h" #include "otautil/thermalutil.h" +#include "package.h" #include "private/install.h" #include "roots.h" #include "ui.h" @@ -67,18 +65,7 @@ static constexpr float VERIFICATION_PROGRESS_FRACTION = 0.25; static std::condition_variable finish_log_temperature; -// This function parses and returns the build.version.incremental -static std::string parse_build_number(const std::string& str) { - size_t pos = str.find('='); - if (pos != std::string::npos) { - return android::base::Trim(str.substr(pos+1)); - } - - LOG(ERROR) << "Failed to parse build number in " << str; - return ""; -} - -bool read_metadata_from_package(ZipArchiveHandle zip, std::string* metadata) { +bool ReadMetadataFromPackage(ZipArchiveHandle zip, std::map<std::string, std::string>* metadata) { CHECK(metadata != nullptr); static constexpr const char* METADATA_PATH = "META-INF/com/android/metadata"; @@ -90,101 +77,79 @@ bool read_metadata_from_package(ZipArchiveHandle zip, std::string* metadata) { } uint32_t length = entry.uncompressed_length; - metadata->resize(length, '\0'); - int32_t err = ExtractToMemory(zip, &entry, reinterpret_cast<uint8_t*>(&(*metadata)[0]), length); + std::string metadata_string(length, '\0'); + int32_t err = + ExtractToMemory(zip, &entry, reinterpret_cast<uint8_t*>(&metadata_string[0]), length); if (err != 0) { LOG(ERROR) << "Failed to extract " << METADATA_PATH << ": " << ErrorCodeString(err); return false; } - return true; -} - -// Read the build.version.incremental of src/tgt from the metadata and log it to last_install. -static void read_source_target_build(ZipArchiveHandle zip, std::vector<std::string>* log_buffer) { - std::string metadata; - if (!read_metadata_from_package(zip, &metadata)) { - return; - } - // Examples of the pre-build and post-build strings in metadata: - // pre-build-incremental=2943039 - // post-build-incremental=2951741 - std::vector<std::string> lines = android::base::Split(metadata, "\n"); - for (const std::string& line : lines) { - std::string str = android::base::Trim(line); - if (android::base::StartsWith(str, "pre-build-incremental")) { - std::string source_build = parse_build_number(str); - if (!source_build.empty()) { - log_buffer->push_back("source_build: " + source_build); - } - } else if (android::base::StartsWith(str, "post-build-incremental")) { - std::string target_build = parse_build_number(str); - if (!target_build.empty()) { - log_buffer->push_back("target_build: " + target_build); - } - } - } -} -// Parses the metadata of the OTA package in |zip| and checks whether we are allowed to accept this -// A/B package. Downgrading is not allowed unless explicitly enabled in the package and only for -// incremental packages. -static int check_newer_ab_build(ZipArchiveHandle zip) { - std::string metadata_str; - if (!read_metadata_from_package(zip, &metadata_str)) { - return INSTALL_CORRUPT; - } - std::map<std::string, std::string> metadata; - for (const std::string& line : android::base::Split(metadata_str, "\n")) { + for (const std::string& line : android::base::Split(metadata_string, "\n")) { size_t eq = line.find('='); if (eq != std::string::npos) { - metadata[line.substr(0, eq)] = line.substr(eq + 1); + metadata->emplace(android::base::Trim(line.substr(0, eq)), + android::base::Trim(line.substr(eq + 1))); } } - std::string value = android::base::GetProperty("ro.product.device", ""); - const std::string& pkg_device = metadata["pre-device"]; - if (pkg_device != value || pkg_device.empty()) { - LOG(ERROR) << "Package is for product " << pkg_device << " but expected " << value; - return INSTALL_ERROR; + return true; +} + +// Gets the value for the given key in |metadata|. Returns an emtpy string if the key isn't +// present. +static std::string get_value(const std::map<std::string, std::string>& metadata, + const std::string& key) { + const auto& it = metadata.find(key); + return (it == metadata.end()) ? "" : it->second; +} + +static std::string OtaTypeToString(OtaType type) { + switch (type) { + case OtaType::AB: + return "AB"; + case OtaType::BLOCK: + return "BLOCK"; + case OtaType::BRICK: + return "BRICK"; } +} - // We allow the package to not have any serialno; and we also allow it to carry multiple serial - // numbers split by "|"; e.g. serialno=serialno1|serialno2|serialno3 ... We will fail the - // verification if the device's serialno doesn't match any of these carried numbers. - value = android::base::GetProperty("ro.serialno", ""); - const std::string& pkg_serial_no = metadata["serialno"]; - if (!pkg_serial_no.empty()) { - bool match = false; - for (const std::string& number : android::base::Split(pkg_serial_no, "|")) { - if (value == android::base::Trim(number)) { - match = true; - break; - } - } - if (!match) { - LOG(ERROR) << "Package is for serial " << pkg_serial_no; - return INSTALL_ERROR; - } +// Read the build.version.incremental of src/tgt from the metadata and log it to last_install. +static void ReadSourceTargetBuild(const std::map<std::string, std::string>& metadata, + std::vector<std::string>* log_buffer) { + // Examples of the pre-build and post-build strings in metadata: + // pre-build-incremental=2943039 + // post-build-incremental=2951741 + auto source_build = get_value(metadata, "pre-build-incremental"); + if (!source_build.empty()) { + log_buffer->push_back("source_build: " + source_build); } - if (metadata["ota-type"] != "AB") { - LOG(ERROR) << "Package is not A/B"; - return INSTALL_ERROR; + auto target_build = get_value(metadata, "post-build-incremental"); + if (!target_build.empty()) { + log_buffer->push_back("target_build: " + target_build); } +} +// Checks the build version, fingerprint and timestamp in the metadata of the A/B package. +// Downgrading is not allowed unless explicitly enabled in the package and only for +// incremental packages. +static int CheckAbSpecificMetadata(const std::map<std::string, std::string>& metadata) { // Incremental updates should match the current build. - value = android::base::GetProperty("ro.build.version.incremental", ""); - const std::string& pkg_pre_build = metadata["pre-build-incremental"]; - if (!pkg_pre_build.empty() && pkg_pre_build != value) { - LOG(ERROR) << "Package is for source build " << pkg_pre_build << " but expected " << value; + auto device_pre_build = android::base::GetProperty("ro.build.version.incremental", ""); + auto pkg_pre_build = get_value(metadata, "pre-build-incremental"); + if (!pkg_pre_build.empty() && pkg_pre_build != device_pre_build) { + LOG(ERROR) << "Package is for source build " << pkg_pre_build << " but expected " + << device_pre_build; return INSTALL_ERROR; } - value = android::base::GetProperty("ro.build.fingerprint", ""); - const std::string& pkg_pre_build_fingerprint = metadata["pre-build"]; - if (!pkg_pre_build_fingerprint.empty() && pkg_pre_build_fingerprint != value) { + auto device_fingerprint = android::base::GetProperty("ro.build.fingerprint", ""); + auto pkg_pre_build_fingerprint = get_value(metadata, "pre-build"); + if (!pkg_pre_build_fingerprint.empty() && pkg_pre_build_fingerprint != device_fingerprint) { LOG(ERROR) << "Package is for source build " << pkg_pre_build_fingerprint << " but expected " - << value; + << device_fingerprint; return INSTALL_ERROR; } @@ -194,10 +159,11 @@ static int check_newer_ab_build(ZipArchiveHandle zip) { int64_t pkg_post_timestamp = 0; // We allow to full update to the same version we are running, in case there // is a problem with the current copy of that version. - if (metadata["post-timestamp"].empty() || - !android::base::ParseInt(metadata["post-timestamp"].c_str(), &pkg_post_timestamp) || + auto pkg_post_timestamp_string = get_value(metadata, "post-timestamp"); + if (pkg_post_timestamp_string.empty() || + !android::base::ParseInt(pkg_post_timestamp_string, &pkg_post_timestamp) || pkg_post_timestamp < build_timestamp) { - if (metadata["ota-downgrade"] != "yes") { + if (get_value(metadata, "ota-downgrade") != "yes") { LOG(ERROR) << "Update package is older than the current build, expected a build " "newer than timestamp " << build_timestamp << " but package has timestamp " << pkg_post_timestamp @@ -213,13 +179,55 @@ static int check_newer_ab_build(ZipArchiveHandle zip) { return 0; } +int CheckPackageMetadata(const std::map<std::string, std::string>& metadata, OtaType ota_type) { + auto package_ota_type = get_value(metadata, "ota-type"); + auto expected_ota_type = OtaTypeToString(ota_type); + if (ota_type != OtaType::AB && ota_type != OtaType::BRICK) { + LOG(INFO) << "Skip package metadata check for ota type " << expected_ota_type; + return 0; + } + + if (package_ota_type != expected_ota_type) { + LOG(ERROR) << "Unexpected ota package type, expects " << expected_ota_type << ", actual " + << package_ota_type; + return INSTALL_ERROR; + } + + auto device = android::base::GetProperty("ro.product.device", ""); + auto pkg_device = get_value(metadata, "pre-device"); + if (pkg_device != device || pkg_device.empty()) { + LOG(ERROR) << "Package is for product " << pkg_device << " but expected " << device; + return INSTALL_ERROR; + } + + // We allow the package to not have any serialno; and we also allow it to carry multiple serial + // numbers split by "|"; e.g. serialno=serialno1|serialno2|serialno3 ... We will fail the + // verification if the device's serialno doesn't match any of these carried numbers. + auto pkg_serial_no = get_value(metadata, "serialno"); + if (!pkg_serial_no.empty()) { + auto device_serial_no = android::base::GetProperty("ro.serialno", ""); + bool serial_number_match = false; + for (const auto& number : android::base::Split(pkg_serial_no, "|")) { + if (device_serial_no == android::base::Trim(number)) { + serial_number_match = true; + } + } + if (!serial_number_match) { + LOG(ERROR) << "Package is for serial " << pkg_serial_no; + return INSTALL_ERROR; + } + } + + if (ota_type == OtaType::AB) { + return CheckAbSpecificMetadata(metadata); + } + + return 0; +} + int SetUpAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int status_fd, std::vector<std::string>* cmd) { CHECK(cmd != nullptr); - int ret = check_newer_ab_build(zip); - if (ret != 0) { - return ret; - } // For A/B updates we extract the payload properties to a buffer and obtain the RAW payload offset // in the zip file. @@ -285,6 +293,11 @@ int SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, i return INSTALL_ERROR; } + // When executing the update binary contained in the package, the arguments passed are: + // - the version number for this interface + // - an FD to which the program can write in order to update the progress bar. + // - the name of the package zip file. + // - an optional argument "retry" if this update is a retry of a failed update attempt. *cmd = { binary_path, std::to_string(kRecoveryApiVersion), @@ -311,82 +324,72 @@ static void log_max_temperature(int* max_temperature, const std::atomic<bool>& l static int try_update_binary(const std::string& package, ZipArchiveHandle zip, bool* wipe_cache, std::vector<std::string>* log_buffer, int retry_count, int* max_temperature) { - read_source_target_build(zip, log_buffer); - - int pipefd[2]; - pipe(pipefd); + std::map<std::string, std::string> metadata; + if (!ReadMetadataFromPackage(zip, &metadata)) { + LOG(ERROR) << "Failed to parse metadata in the zip file"; + return INSTALL_CORRUPT; + } bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false); - std::vector<std::string> args; - int ret = is_ab ? SetUpAbUpdateCommands(package, zip, pipefd[1], &args) - : SetUpNonAbUpdateCommands(package, zip, retry_count, pipefd[1], &args); - if (ret) { - close(pipefd[0]); - close(pipefd[1]); + // Verifies against the metadata in the package first. + if (int check_status = is_ab ? CheckPackageMetadata(metadata, OtaType::AB) : 0; + check_status != 0) { log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure)); - return ret; + return check_status; } - // When executing the update binary contained in the package, the - // arguments passed are: - // - // - the version number for this interface - // - // - an FD to which the program can write in order to update the - // progress bar. The program can write single-line commands: - // - // progress <frac> <secs> - // fill up the next <frac> part of of the progress bar - // over <secs> seconds. If <secs> is zero, use - // set_progress commands to manually control the - // progress of this segment of the bar. - // - // set_progress <frac> - // <frac> should be between 0.0 and 1.0; sets the - // progress bar within the segment defined by the most - // recent progress command. + ReadSourceTargetBuild(metadata, log_buffer); + + // The updater in child process writes to the pipe to communicate with recovery. + android::base::unique_fd pipe_read, pipe_write; + if (!android::base::Pipe(&pipe_read, &pipe_write)) { + PLOG(ERROR) << "Failed to create pipe for updater-recovery communication"; + return INSTALL_CORRUPT; + } + + // The updater-recovery communication protocol. // - // ui_print <string> - // display <string> on the screen. + // progress <frac> <secs> + // fill up the next <frac> part of of the progress bar over <secs> seconds. If <secs> is + // zero, use `set_progress` commands to manually control the progress of this segment of the + // bar. // - // wipe_cache - // a wipe of cache will be performed following a successful - // installation. + // set_progress <frac> + // <frac> should be between 0.0 and 1.0; sets the progress bar within the segment defined by + // the most recent progress command. // - // clear_display - // turn off the text display. + // ui_print <string> + // display <string> on the screen. // - // enable_reboot - // packages can explicitly request that they want the user - // to be able to reboot during installation (useful for - // debugging packages that don't exit). + // wipe_cache + // a wipe of cache will be performed following a successful installation. // - // retry_update - // updater encounters some issue during the update. It requests - // a reboot to retry the same package automatically. + // clear_display + // turn off the text display. // - // log <string> - // updater requests logging the string (e.g. cause of the - // failure). + // enable_reboot + // packages can explicitly request that they want the user to be able to reboot during + // installation (useful for debugging packages that don't exit). // - // - the name of the package zip file. + // retry_update + // updater encounters some issue during the update. It requests a reboot to retry the same + // package automatically. // - // - an optional argument "retry" if this update is a retry of a failed - // update attempt. + // log <string> + // updater requests logging the string (e.g. cause of the failure). // - // Convert the vector to a NULL-terminated char* array suitable for execv. - const char* chr_args[args.size() + 1]; - chr_args[args.size()] = nullptr; - for (size_t i = 0; i < args.size(); i++) { - chr_args[i] = args[i].c_str(); + std::vector<std::string> args; + if (int update_status = + is_ab ? SetUpAbUpdateCommands(package, zip, pipe_write.get(), &args) + : SetUpNonAbUpdateCommands(package, zip, retry_count, pipe_write.get(), &args); + update_status != 0) { + log_buffer->push_back(android::base::StringPrintf("error: %d", kUpdateBinaryCommandFailure)); + return update_status; } pid_t pid = fork(); - if (pid == -1) { - close(pipefd[0]); - close(pipefd[1]); PLOG(ERROR) << "Failed to fork update binary"; log_buffer->push_back(android::base::StringPrintf("error: %d", kForkUpdateBinaryFailure)); return INSTALL_ERROR; @@ -394,16 +397,18 @@ static int try_update_binary(const std::string& package, ZipArchiveHandle zip, b if (pid == 0) { umask(022); - close(pipefd[0]); - execv(chr_args[0], const_cast<char**>(chr_args)); - // Bug: 34769056 - // We shouldn't use LOG/PLOG in the forked process, since they may cause - // the child process to hang. This deadlock results from an improperly - // copied mutex in the ui functions. + pipe_read.reset(); + + // Convert the std::string vector to a NULL-terminated char* vector suitable for execv. + auto chr_args = StringVectorToNullTerminatedArray(args); + execv(chr_args[0], chr_args.data()); + // We shouldn't use LOG/PLOG in the forked process, since they may cause the child process to + // hang. This deadlock results from an improperly copied mutex in the ui functions. + // (Bug: 34769056) fprintf(stdout, "E:Can't run %s (%s)\n", chr_args[0], strerror(errno)); _exit(EXIT_FAILURE); } - close(pipefd[1]); + pipe_write.reset(); std::atomic<bool> logger_finished(false); std::thread temperature_logger(log_max_temperature, max_temperature, std::ref(logger_finished)); @@ -412,7 +417,7 @@ static int try_update_binary(const std::string& package, ZipArchiveHandle zip, b bool retry_update = false; char buffer[1024]; - FILE* from_child = fdopen(pipefd[0], "r"); + FILE* from_child = android::base::Fdopen(std::move(pipe_read), "r"); while (fgets(buffer, sizeof(buffer), from_child) != nullptr) { std::string line(buffer); size_t space = line.find_first_of(" \n"); @@ -477,9 +482,16 @@ static int try_update_binary(const std::string& package, ZipArchiveHandle zip, b if (retry_update) { return INSTALL_RETRY; } - if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - LOG(ERROR) << "Error in " << package << " (Status " << WEXITSTATUS(status) << ")"; + if (WIFEXITED(status)) { + if (WEXITSTATUS(status) != EXIT_SUCCESS) { + LOG(ERROR) << "Error in " << package << " (status " << WEXITSTATUS(status) << ")"; + return INSTALL_ERROR; + } + } else if (WIFSIGNALED(status)) { + LOG(ERROR) << "Error in " << package << " (killed by signal " << WTERMSIG(status) << ")"; return INSTALL_ERROR; + } else { + LOG(FATAL) << "Invalid status code " << status; } return INSTALL_SUCCESS; @@ -568,40 +580,35 @@ static int really_install_package(const std::string& path, bool* wipe_cache, boo if (needs_mount) { if (path[0] == '@') { - ensure_path_mounted(path.substr(1).c_str()); + ensure_path_mounted(path.substr(1)); } else { - ensure_path_mounted(path.c_str()); + ensure_path_mounted(path); } } - MemMapping map; - if (!map.MapFile(path)) { - LOG(ERROR) << "failed to map file"; + auto package = Package::CreateMemoryPackage( + path, std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1)); + if (!package) { log_buffer->push_back(android::base::StringPrintf("error: %d", kMapFileFailure)); return INSTALL_CORRUPT; } // Verify package. - if (!verify_package(map.addr, map.length)) { + if (!verify_package(package.get())) { log_buffer->push_back(android::base::StringPrintf("error: %d", kZipVerificationFailure)); return INSTALL_CORRUPT; } // Try to open the package. - ZipArchiveHandle zip; - int err = OpenArchiveFromMemory(map.addr, map.length, path.c_str(), &zip); - if (err != 0) { - LOG(ERROR) << "Can't open " << path << " : " << ErrorCodeString(err); + ZipArchiveHandle zip = package->GetZipArchiveHandle(); + if (!zip) { log_buffer->push_back(android::base::StringPrintf("error: %d", kZipOpenFailure)); - - CloseArchive(zip); return INSTALL_CORRUPT; } // Additionally verify the compatibility of the package. if (!verify_package_compatibility(zip)) { log_buffer->push_back(android::base::StringPrintf("error: %d", kPackageCompatibilityFailure)); - CloseArchive(zip); return INSTALL_CORRUPT; } @@ -615,7 +622,6 @@ static int really_install_package(const std::string& path, bool* wipe_cache, boo ui->SetEnableReboot(true); ui->Print("\n"); - CloseArchive(zip); return result; } @@ -694,20 +700,19 @@ int install_package(const std::string& path, bool* wipe_cache, bool needs_mount, return result; } -bool verify_package(const unsigned char* package_data, size_t package_size) { - static constexpr const char* PUBLIC_KEYS_FILE = "/res/keys"; - std::vector<Certificate> loadedKeys; - if (!load_keys(PUBLIC_KEYS_FILE, loadedKeys)) { +bool verify_package(Package* package) { + static constexpr const char* CERTIFICATE_ZIP_FILE = "/system/etc/security/otacerts.zip"; + std::vector<Certificate> loaded_keys = LoadKeysFromZipfile(CERTIFICATE_ZIP_FILE); + if (loaded_keys.empty()) { LOG(ERROR) << "Failed to load keys"; return false; } - LOG(INFO) << loadedKeys.size() << " key(s) loaded from " << PUBLIC_KEYS_FILE; + LOG(INFO) << loaded_keys.size() << " key(s) loaded from " << CERTIFICATE_ZIP_FILE; // Verify package. ui->Print("Verifying update package...\n"); auto t0 = std::chrono::system_clock::now(); - int err = verify_file(package_data, package_size, loadedKeys, - std::bind(&RecoveryUI::SetProgress, ui, std::placeholders::_1)); + int err = verify_file(package, loaded_keys); std::chrono::duration<double> duration = std::chrono::system_clock::now() - t0; ui->Print("Update package verification took %.1f s (result %d).\n", duration.count(), err); if (err != VERIFY_SUCCESS) { @@ -19,28 +19,53 @@ #include <stddef.h> +#include <map> #include <string> +#include <vector> #include <ziparchive/zip_archive.h> -enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED, - INSTALL_RETRY }; +#include "package.h" + +enum InstallResult { + INSTALL_SUCCESS, + INSTALL_ERROR, + INSTALL_CORRUPT, + INSTALL_NONE, + INSTALL_SKIPPED, + INSTALL_RETRY, + INSTALL_KEY_INTERRUPTED +}; + +enum class OtaType { + AB, + BLOCK, + BRICK, +}; // Installs the given update package. If INSTALL_SUCCESS is returned and *wipe_cache is true on // exit, caller should wipe the cache partition. int install_package(const std::string& package, bool* wipe_cache, bool needs_mount, int retry_count); -// Verify the package by ota keys. Return true if the package is verified successfully, -// otherwise return false. -bool verify_package(const unsigned char* package_data, size_t package_size); +// Verifies the package by ota keys. Returns true if the package is verified successfully, +// otherwise returns false. +bool verify_package(Package* package); -// Read meta data file of the package, write its content in the string pointed by meta_data. -// Return true if succeed, otherwise return false. -bool read_metadata_from_package(ZipArchiveHandle zip, std::string* metadata); +// Reads meta data file of the package; parses each line in the format "key=value"; and writes the +// result to |metadata|. Return true if succeed, otherwise return false. +bool ReadMetadataFromPackage(ZipArchiveHandle zip, std::map<std::string, std::string>* metadata); + +// Reads the "recovery.wipe" entry in the zip archive returns a list of partitions to wipe. +std::vector<std::string> GetWipePartitionList(Package* wipe_package); // Verifies the compatibility info in a Treble-compatible package. Returns true directly if the // entry doesn't exist. bool verify_package_compatibility(ZipArchiveHandle package_zip); +// Checks if the the metadata in the OTA package has expected values. Returns 0 on success. +// Mandatory checks: ota-type, pre-device and serial number(if presents) +// AB OTA specific checks: pre-build version, fingerprint, timestamp. +int CheckPackageMetadata(const std::map<std::string, std::string>& metadata, OtaType ota_type); + #endif // RECOVERY_INSTALL_H_ diff --git a/logging.cpp b/logging.cpp index d5af72aad..283d11507 100644 --- a/logging.cpp +++ b/logging.cpp @@ -46,7 +46,7 @@ static const std::string LAST_LOG_FILTER = "recovery/last_log"; // fopen(3)'s the given file, by mounting volumes and making parent dirs as necessary. Returns the // file pointer, or nullptr on error. static FILE* fopen_path(const std::string& path, const char* mode) { - if (ensure_path_mounted(path.c_str()) != 0) { + if (ensure_path_mounted(path) != 0) { LOG(ERROR) << "Can't mount " << path; return nullptr; } @@ -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..a95d979a5 100644 --- a/minadbd/Android.bp +++ b/minadbd/Android.bp @@ -21,6 +21,8 @@ cc_defaults { "-Werror", ], + cpp_std: "experimental", + include_dirs: [ "system/core/adb", ], @@ -52,6 +54,7 @@ cc_library { cc_test { name: "minadbd_test", + isolated: true, defaults: [ "minadbd_defaults", @@ -64,7 +67,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..9309ed749 100644 --- a/minadbd/minadbd_services.cpp +++ b/minadbd/minadbd_services.cpp @@ -23,6 +23,7 @@ #include <functional> #include <string> +#include <string_view> #include <thread> #include "adb.h" @@ -33,29 +34,29 @@ #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 */) { - if (!strncmp(name, "sideload:", 9)) { - // this exit status causes recovery to print a special error - // message saying to use a newer adb (that supports - // sideload-host). +unique_fd daemon_service_to_fd(std::string_view name, atransport* /* transport */) { + if (name.starts_with("sideload:")) { + // This exit status causes recovery to print a special error message saying to use a newer adb + // (that supports sideload-host). exit(3); - } else if (!strncmp(name, "sideload-host:", 14)) { - std::string arg(name + 14); + } else if (name.starts_with("sideload-host:")) { + std::string arg(name.substr(strlen("sideload-host:"))); return create_service_thread("sideload-host", std::bind(sideload_host_service, std::placeholders::_1, arg)); } diff --git a/minui/events.cpp b/minui/events.cpp index 2894c3b6b..7d0250e97 100644 --- a/minui/events.cpp +++ b/minui/events.cpp @@ -23,144 +23,148 @@ #include <string.h> #include <sys/epoll.h> #include <sys/ioctl.h> +#include <sys/types.h> #include <unistd.h> #include <functional> +#include <memory> + +#include <android-base/unique_fd.h> #include "minui/minui.h" -#define MAX_DEVICES 16 -#define MAX_MISC_FDS 16 +constexpr size_t MAX_DEVICES = 16; +constexpr size_t MAX_MISC_FDS = 16; -#define BITS_PER_LONG (sizeof(unsigned long) * 8) -#define BITS_TO_LONGS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG) +constexpr size_t BITS_PER_LONG = sizeof(unsigned long) * 8; +constexpr size_t BITS_TO_LONGS(size_t bits) { + return ((bits + BITS_PER_LONG - 1) / BITS_PER_LONG); +} -struct fd_info { - int fd; +struct FdInfo { + android::base::unique_fd fd; ev_callback cb; }; -static int g_epoll_fd; -static epoll_event polledevents[MAX_DEVICES + MAX_MISC_FDS]; -static int npolledevents; +static android::base::unique_fd g_epoll_fd; +static epoll_event g_polled_events[MAX_DEVICES + MAX_MISC_FDS]; +static int g_polled_events_count; -static fd_info ev_fdinfo[MAX_DEVICES + MAX_MISC_FDS]; +static FdInfo ev_fdinfo[MAX_DEVICES + MAX_MISC_FDS]; -static unsigned ev_count = 0; -static unsigned ev_dev_count = 0; -static unsigned ev_misc_count = 0; +static size_t g_ev_count = 0; +static size_t g_ev_dev_count = 0; +static size_t g_ev_misc_count = 0; static bool test_bit(size_t bit, unsigned long* array) { // NOLINT - return (array[bit/BITS_PER_LONG] & (1UL << (bit % BITS_PER_LONG))) != 0; + return (array[bit / BITS_PER_LONG] & (1UL << (bit % BITS_PER_LONG))) != 0; } int ev_init(ev_callback input_cb, bool allow_touch_inputs) { - g_epoll_fd = epoll_create(MAX_DEVICES + MAX_MISC_FDS); - if (g_epoll_fd == -1) { + g_epoll_fd.reset(); + + android::base::unique_fd epoll_fd(epoll_create1(EPOLL_CLOEXEC)); + if (epoll_fd == -1) { return -1; } - bool epollctlfail = false; - DIR* dir = opendir("/dev/input"); - if (dir != nullptr) { - dirent* de; - while ((de = readdir(dir))) { - if (strncmp(de->d_name, "event", 5)) continue; - int fd = openat(dirfd(dir), de->d_name, O_RDONLY); - if (fd == -1) continue; - - // Use unsigned long to match ioctl's parameter type. - unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT - - // Read the evbits of the input device. - if (ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) { - close(fd); - continue; - } + std::unique_ptr<DIR, decltype(&closedir)> dir(opendir("/dev/input"), closedir); + if (!dir) { + return -1; + } - // We assume that only EV_KEY, EV_REL, and EV_SW event types are ever needed. EV_ABS is also - // allowed if allow_touch_inputs is set. - if (!test_bit(EV_KEY, ev_bits) && !test_bit(EV_REL, ev_bits) && !test_bit(EV_SW, ev_bits)) { - if (!allow_touch_inputs || !test_bit(EV_ABS, ev_bits)) { - close(fd); - continue; - } - } + bool epoll_ctl_failed = false; + dirent* de; + while ((de = readdir(dir.get())) != nullptr) { + if (strncmp(de->d_name, "event", 5)) continue; + android::base::unique_fd fd(openat(dirfd(dir.get()), de->d_name, O_RDONLY | O_CLOEXEC)); + if (fd == -1) continue; + + // Use unsigned long to match ioctl's parameter type. + unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT - epoll_event ev; - ev.events = EPOLLIN | EPOLLWAKEUP; - ev.data.ptr = &ev_fdinfo[ev_count]; - if (epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { - close(fd); - epollctlfail = true; + // Read the evbits of the input device. + if (ioctl(fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) { + continue; + } + + // We assume that only EV_KEY, EV_REL, and EV_SW event types are ever needed. EV_ABS is also + // allowed if allow_touch_inputs is set. + if (!test_bit(EV_KEY, ev_bits) && !test_bit(EV_REL, ev_bits) && !test_bit(EV_SW, ev_bits)) { + if (!allow_touch_inputs || !test_bit(EV_ABS, ev_bits)) { continue; } + } - ev_fdinfo[ev_count].fd = fd; - ev_fdinfo[ev_count].cb = std::move(input_cb); - ev_count++; - ev_dev_count++; - if (ev_dev_count == MAX_DEVICES) break; + epoll_event ev; + ev.events = EPOLLIN | EPOLLWAKEUP; + ev.data.ptr = &ev_fdinfo[g_ev_count]; + if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &ev) == -1) { + epoll_ctl_failed = true; + continue; } - closedir(dir); + ev_fdinfo[g_ev_count].fd.reset(fd.release()); + ev_fdinfo[g_ev_count].cb = input_cb; + g_ev_count++; + g_ev_dev_count++; + if (g_ev_dev_count == MAX_DEVICES) break; } - if (epollctlfail && !ev_count) { - close(g_epoll_fd); - g_epoll_fd = -1; + if (epoll_ctl_failed && !g_ev_count) { return -1; } + g_epoll_fd.reset(epoll_fd.release()); return 0; } int ev_get_epollfd(void) { - return g_epoll_fd; + return g_epoll_fd.get(); } -int ev_add_fd(int fd, ev_callback cb) { - if (ev_misc_count == MAX_MISC_FDS || cb == NULL) { +int ev_add_fd(android::base::unique_fd&& fd, ev_callback cb) { + if (g_ev_misc_count == MAX_MISC_FDS || cb == nullptr) { return -1; } epoll_event ev; ev.events = EPOLLIN | EPOLLWAKEUP; - ev.data.ptr = static_cast<void*>(&ev_fdinfo[ev_count]); + ev.data.ptr = static_cast<void*>(&ev_fdinfo[g_ev_count]); int ret = epoll_ctl(g_epoll_fd, EPOLL_CTL_ADD, fd, &ev); if (!ret) { - ev_fdinfo[ev_count].fd = fd; - ev_fdinfo[ev_count].cb = std::move(cb); - ev_count++; - ev_misc_count++; + ev_fdinfo[g_ev_count].fd.reset(fd.release()); + ev_fdinfo[g_ev_count].cb = std::move(cb); + g_ev_count++; + g_ev_misc_count++; } return ret; } void ev_exit(void) { - while (ev_count > 0) { - close(ev_fdinfo[--ev_count].fd); - } - ev_misc_count = 0; - ev_dev_count = 0; - close(g_epoll_fd); + while (g_ev_count > 0) { + ev_fdinfo[--g_ev_count].fd.reset(); + } + g_ev_misc_count = 0; + g_ev_dev_count = 0; + g_epoll_fd.reset(); } int ev_wait(int timeout) { - npolledevents = epoll_wait(g_epoll_fd, polledevents, ev_count, timeout); - if (npolledevents <= 0) { - return -1; - } - return 0; + g_polled_events_count = epoll_wait(g_epoll_fd, g_polled_events, g_ev_count, timeout); + if (g_polled_events_count <= 0) { + return -1; + } + return 0; } void ev_dispatch(void) { - for (int n = 0; n < npolledevents; n++) { - fd_info* fdi = static_cast<fd_info*>(polledevents[n].data.ptr); + for (int n = 0; n < g_polled_events_count; n++) { + FdInfo* fdi = static_cast<FdInfo*>(g_polled_events[n].data.ptr); const ev_callback& cb = fdi->cb; if (cb) { - cb(fdi->fd, polledevents[n].events); + cb(fdi->fd, g_polled_events[n].events); } } } @@ -180,7 +184,7 @@ int ev_sync_key_state(const ev_set_key_callback& set_key_cb) { unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT unsigned long key_bits[BITS_TO_LONGS(KEY_MAX)]; // NOLINT - for (size_t i = 0; i < ev_dev_count; ++i) { + for (size_t i = 0; i < g_ev_dev_count; ++i) { memset(ev_bits, 0, sizeof(ev_bits)); memset(key_bits, 0, sizeof(key_bits)); @@ -205,37 +209,36 @@ int ev_sync_key_state(const ev_set_key_callback& set_key_cb) { } void ev_iterate_available_keys(const std::function<void(int)>& f) { - // Use unsigned long to match ioctl's parameter type. - unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT - unsigned long key_bits[BITS_TO_LONGS(KEY_MAX)]; // NOLINT + // Use unsigned long to match ioctl's parameter type. + unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)]; // NOLINT + unsigned long key_bits[BITS_TO_LONGS(KEY_MAX)]; // NOLINT - for (size_t i = 0; i < ev_dev_count; ++i) { - memset(ev_bits, 0, sizeof(ev_bits)); - memset(key_bits, 0, sizeof(key_bits)); + for (size_t i = 0; i < g_ev_dev_count; ++i) { + memset(ev_bits, 0, sizeof(ev_bits)); + memset(key_bits, 0, sizeof(key_bits)); - // Does this device even have keys? - if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) { - continue; - } - if (!test_bit(EV_KEY, ev_bits)) { - continue; - } + // Does this device even have keys? + if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) { + continue; + } + if (!test_bit(EV_KEY, ev_bits)) { + continue; + } - int rc = ioctl(ev_fdinfo[i].fd, EVIOCGBIT(EV_KEY, KEY_MAX), key_bits); - if (rc == -1) { - continue; - } + if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(EV_KEY, KEY_MAX), key_bits) == -1) { + continue; + } - for (int key_code = 0; key_code <= KEY_MAX; ++key_code) { - if (test_bit(key_code, key_bits)) { - f(key_code); - } - } + for (int key_code = 0; key_code <= KEY_MAX; ++key_code) { + if (test_bit(key_code, key_bits)) { + f(key_code); + } } + } } void ev_iterate_touch_inputs(const std::function<void(int)>& action) { - for (size_t i = 0; i < ev_dev_count; ++i) { + for (size_t i = 0; i < g_ev_dev_count; ++i) { // Use unsigned long to match ioctl's parameter type. unsigned long ev_bits[BITS_TO_LONGS(EV_MAX)] = {}; // NOLINT if (ioctl(ev_fdinfo[i].fd, EVIOCGBIT(0, sizeof(ev_bits)), ev_bits) == -1) { diff --git a/minui/graphics.cpp b/minui/graphics.cpp index e6367d950..4d1f9b2d2 100644 --- a/minui/graphics.cpp +++ b/minui/graphics.cpp @@ -40,7 +40,7 @@ static uint32_t gr_current = ~0; static constexpr uint32_t alpha_mask = 0xff000000; // gr_draw is owned by backends. -static const GRSurface* gr_draw = nullptr; +static GRSurface* gr_draw = nullptr; static GRRotation rotation = GRRotation::NONE; static PixelFormat pixel_format = PixelFormat::UNKNOWN; @@ -121,28 +121,29 @@ static void incr_y(uint32_t** p, int row_pixels) { } // Returns pixel pointer at given coordinates with rotation adjustment. -static uint32_t* pixel_at(const GRSurface* surf, int x, int y, int row_pixels) { +static uint32_t* PixelAt(GRSurface* surface, int x, int y, int row_pixels) { switch (rotation) { case GRRotation::NONE: - return reinterpret_cast<uint32_t*>(surf->data) + y * row_pixels + x; + return reinterpret_cast<uint32_t*>(surface->data()) + y * row_pixels + x; case GRRotation::RIGHT: - return reinterpret_cast<uint32_t*>(surf->data) + x * row_pixels + (surf->width - y); + return reinterpret_cast<uint32_t*>(surface->data()) + x * row_pixels + (surface->width - y); case GRRotation::DOWN: - return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - y) * row_pixels + - (surf->width - 1 - x); + return reinterpret_cast<uint32_t*>(surface->data()) + (surface->height - 1 - y) * row_pixels + + (surface->width - 1 - x); case GRRotation::LEFT: - return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - x) * row_pixels + y; + return reinterpret_cast<uint32_t*>(surface->data()) + (surface->height - 1 - x) * row_pixels + + y; default: printf("invalid rotation %d", static_cast<int>(rotation)); } return nullptr; } -static void text_blend(uint8_t* src_p, int src_row_bytes, uint32_t* dst_p, int dst_row_pixels, - int width, int height) { +static void TextBlend(const uint8_t* src_p, int src_row_bytes, uint32_t* dst_p, int dst_row_pixels, + int width, int height) { uint8_t alpha_current = static_cast<uint8_t>((alpha_mask & gr_current) >> 24); for (int j = 0; j < height; ++j) { - uint8_t* sx = src_p; + const uint8_t* sx = src_p; uint32_t* px = dst_p; for (int i = 0; i < width; ++i, incr_x(&px, dst_row_pixels)) { uint8_t a = *sx++; @@ -176,18 +177,18 @@ void gr_text(const GRFont* font, int x, int y, const char* s, bool bold) { } int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes; - uint8_t* src_p = font->texture->data + ((ch - ' ') * font->char_width) + - (bold ? font->char_height * font->texture->row_bytes : 0); - uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels); + const uint8_t* src_p = font->texture->data() + ((ch - ' ') * font->char_width) + + (bold ? font->char_height * font->texture->row_bytes : 0); + uint32_t* dst_p = PixelAt(gr_draw, x, y, row_pixels); - text_blend(src_p, font->texture->row_bytes, dst_p, row_pixels, font->char_width, - font->char_height); + TextBlend(src_p, font->texture->row_bytes, dst_p, row_pixels, font->char_width, + font->char_height); x += font->char_width; } } -void gr_texticon(int x, int y, GRSurface* icon) { +void gr_texticon(int x, int y, const GRSurface* icon) { if (icon == nullptr) return; if (icon->pixel_bytes != 1) { @@ -201,10 +202,9 @@ void gr_texticon(int x, int y, GRSurface* icon) { if (outside(x, y) || outside(x + icon->width - 1, y + icon->height - 1)) return; int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes; - uint8_t* src_p = icon->data; - uint32_t* dst_p = pixel_at(gr_draw, x, y, row_pixels); - - text_blend(src_p, icon->row_bytes, dst_p, row_pixels, icon->width, icon->height); + const uint8_t* src_p = icon->data(); + uint32_t* dst_p = PixelAt(gr_draw, x, y, row_pixels); + TextBlend(src_p, icon->row_bytes, dst_p, row_pixels, icon->width, icon->height); } void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a) { @@ -221,9 +221,9 @@ void gr_clear() { (gr_current & 0xff) == ((gr_current >> 16) & 0xff) && (gr_current & 0xff) == ((gr_current >> 24) & 0xff) && gr_draw->row_bytes == gr_draw->width * gr_draw->pixel_bytes) { - memset(gr_draw->data, gr_current & 0xff, gr_draw->height * gr_draw->row_bytes); + memset(gr_draw->data(), gr_current & 0xff, gr_draw->height * gr_draw->row_bytes); } else { - uint32_t* px = reinterpret_cast<uint32_t*>(gr_draw->data); + uint32_t* px = reinterpret_cast<uint32_t*>(gr_draw->data()); int row_diff = gr_draw->row_bytes / gr_draw->pixel_bytes - gr_draw->width; for (int y = 0; y < gr_draw->height; ++y) { for (int x = 0; x < gr_draw->width; ++x) { @@ -244,7 +244,7 @@ void gr_fill(int x1, int y1, int x2, int y2) { if (outside(x1, y1) || outside(x2 - 1, y2 - 1)) return; int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes; - uint32_t* p = pixel_at(gr_draw, x1, y1, row_pixels); + uint32_t* p = PixelAt(gr_draw, x1, y1, row_pixels); uint8_t alpha = static_cast<uint8_t>(((gr_current & alpha_mask) >> 24)); if (alpha > 0) { for (int y = y1; y < y2; ++y) { @@ -258,7 +258,7 @@ void gr_fill(int x1, int y1, int x2, int y2) { } } -void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) { +void gr_blit(const GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) { if (source == nullptr) return; if (gr_draw->pixel_bytes != source->pixel_bytes) { @@ -274,11 +274,12 @@ void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) { if (rotation != GRRotation::NONE) { int src_row_pixels = source->row_bytes / source->pixel_bytes; int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes; - uint32_t* src_py = reinterpret_cast<uint32_t*>(source->data) + sy * source->row_bytes / 4 + sx; - uint32_t* dst_py = pixel_at(gr_draw, dx, dy, row_pixels); + const uint32_t* src_py = + reinterpret_cast<const uint32_t*>(source->data()) + sy * source->row_bytes / 4 + sx; + uint32_t* dst_py = PixelAt(gr_draw, dx, dy, row_pixels); for (int y = 0; y < h; y += 1) { - uint32_t* src_px = src_py; + const uint32_t* src_px = src_py; uint32_t* dst_px = dst_py; for (int x = 0; x < w; x += 1) { *dst_px = *src_px++; @@ -288,8 +289,8 @@ void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) { incr_y(&dst_py, row_pixels); } } else { - unsigned char* src_p = source->data + sy * source->row_bytes + sx * source->pixel_bytes; - unsigned char* dst_p = gr_draw->data + dy * gr_draw->row_bytes + dx * gr_draw->pixel_bytes; + const uint8_t* src_p = source->data() + sy * source->row_bytes + sx * source->pixel_bytes; + uint8_t* dst_p = gr_draw->data() + dy * gr_draw->row_bytes + dx * gr_draw->pixel_bytes; for (int i = 0; i < h; ++i) { memcpy(dst_p, src_p, w * source->pixel_bytes); diff --git a/minui/graphics_adf.cpp b/minui/graphics_adf.cpp index 7439df9ac..10cd60709 100644 --- a/minui/graphics_adf.cpp +++ b/minui/graphics_adf.cpp @@ -20,6 +20,7 @@ #include <fcntl.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> #include <sys/mman.h> #include <unistd.h> @@ -28,51 +29,60 @@ #include "minui/minui.h" -MinuiBackendAdf::MinuiBackendAdf() - : intf_fd(-1), dev(), current_surface(0), n_surfaces(0), surfaces() {} - -int MinuiBackendAdf::SurfaceInit(const drm_mode_modeinfo* mode, GRSurfaceAdf* surf) { - *surf = {}; - surf->fence_fd = -1; - surf->fd = adf_interface_simple_buffer_alloc(intf_fd, mode->hdisplay, mode->vdisplay, format, - &surf->offset, &surf->pitch); - if (surf->fd < 0) { - return surf->fd; +GRSurfaceAdf::~GRSurfaceAdf() { + if (mmapped_buffer_) { + munmap(mmapped_buffer_, pitch * height); + } + if (fence_fd != -1) { + close(fence_fd); + } + if (fd != -1) { + close(fd); } +} + +std::unique_ptr<GRSurfaceAdf> GRSurfaceAdf::Create(int intf_fd, const drm_mode_modeinfo* mode, + __u32 format, int* err) { + __u32 offset; + __u32 pitch; + auto fd = adf_interface_simple_buffer_alloc(intf_fd, mode->hdisplay, mode->vdisplay, format, + &offset, &pitch); - surf->width = mode->hdisplay; - surf->height = mode->vdisplay; - surf->row_bytes = surf->pitch; - surf->pixel_bytes = (format == DRM_FORMAT_RGB565) ? 2 : 4; - - surf->data = static_cast<uint8_t*>( - mmap(nullptr, surf->pitch * surf->height, PROT_WRITE, MAP_SHARED, surf->fd, surf->offset)); - if (surf->data == MAP_FAILED) { - int saved_errno = errno; - close(surf->fd); - return -saved_errno; + if (fd < 0) { + *err = fd; + return nullptr; } - return 0; + std::unique_ptr<GRSurfaceAdf> surf = std::unique_ptr<GRSurfaceAdf>( + new GRSurfaceAdf(mode->hdisplay, mode->vdisplay, pitch, (format == DRM_FORMAT_RGB565 ? 2 : 4), + offset, pitch, fd)); + + auto mmapped = + mmap(nullptr, surf->pitch * surf->height, PROT_WRITE, MAP_SHARED, surf->fd, surf->offset); + if (mmapped == MAP_FAILED) { + *err = -errno; + return nullptr; + } + surf->mmapped_buffer_ = static_cast<uint8_t*>(mmapped); + return surf; } +MinuiBackendAdf::MinuiBackendAdf() : intf_fd(-1), dev(), current_surface(0), n_surfaces(0) {} + int MinuiBackendAdf::InterfaceInit() { adf_interface_data intf_data; - int err = adf_get_interface_data(intf_fd, &intf_data); - if (err < 0) return err; + if (int err = adf_get_interface_data(intf_fd, &intf_data); err < 0) return err; - int ret = 0; - err = SurfaceInit(&intf_data.current_mode, &surfaces[0]); - if (err < 0) { - fprintf(stderr, "allocating surface 0 failed: %s\n", strerror(-err)); - ret = err; + int result = 0; + surfaces[0] = GRSurfaceAdf::Create(intf_fd, &intf_data.current_mode, format, &result); + if (!surfaces[0]) { + fprintf(stderr, "Failed to allocate surface 0: %s\n", strerror(-result)); goto done; } - err = SurfaceInit(&intf_data.current_mode, &surfaces[1]); - if (err < 0) { - fprintf(stderr, "allocating surface 1 failed: %s\n", strerror(-err)); - surfaces[1] = {}; + surfaces[1] = GRSurfaceAdf::Create(intf_fd, &intf_data.current_mode, format, &result); + if (!surfaces[1]) { + fprintf(stderr, "Failed to allocate surface 1: %s\n", strerror(-result)); n_surfaces = 1; } else { n_surfaces = 2; @@ -80,7 +90,7 @@ int MinuiBackendAdf::InterfaceInit() { done: adf_free_interface_data(&intf_data); - return ret; + return result; } int MinuiBackendAdf::DeviceInit(adf_device* dev) { @@ -91,7 +101,7 @@ int MinuiBackendAdf::DeviceInit(adf_device* dev) { err = adf_device_attach(dev, eng_id, intf_id); if (err < 0 && err != -EALREADY) return err; - intf_fd = adf_interface_open(dev, intf_id, O_RDWR); + intf_fd = adf_interface_open(dev, intf_id, O_RDWR | O_CLOEXEC); if (intf_fd < 0) return intf_fd; err = InterfaceInit(); @@ -153,12 +163,12 @@ GRSurface* MinuiBackendAdf::Init() { } void MinuiBackendAdf::Sync(GRSurfaceAdf* surf) { - static constexpr unsigned int warningTimeout = 3000; + static constexpr unsigned int kWarningTimeout = 3000; if (surf == nullptr) return; if (surf->fence_fd >= 0) { - int err = sync_wait(surf->fence_fd, warningTimeout); + int err = sync_wait(surf->fence_fd, kWarningTimeout); if (err < 0) { perror("adf sync fence wait error\n"); } @@ -169,31 +179,22 @@ void MinuiBackendAdf::Sync(GRSurfaceAdf* surf) { } GRSurface* MinuiBackendAdf::Flip() { - GRSurfaceAdf* surf = &surfaces[current_surface]; + const auto& surf = surfaces[current_surface]; int fence_fd = adf_interface_simple_post(intf_fd, eng_id, surf->width, surf->height, format, surf->fd, surf->offset, surf->pitch, -1); if (fence_fd >= 0) surf->fence_fd = fence_fd; current_surface = (current_surface + 1) % n_surfaces; - Sync(&surfaces[current_surface]); - return &surfaces[current_surface]; + Sync(surfaces[current_surface].get()); + return surfaces[current_surface].get(); } void MinuiBackendAdf::Blank(bool blank) { adf_interface_blank(intf_fd, blank ? DRM_MODE_DPMS_OFF : DRM_MODE_DPMS_ON); } -void MinuiBackendAdf::SurfaceDestroy(GRSurfaceAdf* surf) { - munmap(surf->data, surf->pitch * surf->height); - close(surf->fence_fd); - close(surf->fd); -} - MinuiBackendAdf::~MinuiBackendAdf() { adf_device_close(&dev); - for (unsigned int i = 0; i < n_surfaces; i++) { - SurfaceDestroy(&surfaces[i]); - } if (intf_fd >= 0) close(intf_fd); } diff --git a/minui/graphics_adf.h b/minui/graphics_adf.h index 2f019ed0b..79d8d2acb 100644 --- a/minui/graphics_adf.h +++ b/minui/graphics_adf.h @@ -14,45 +14,63 @@ * limitations under the License. */ -#ifndef _GRAPHICS_ADF_H_ -#define _GRAPHICS_ADF_H_ +#pragma once + +#include <stddef.h> +#include <stdint.h> +#include <sys/types.h> + +#include <memory> #include <adf/adf.h> #include "graphics.h" +#include "minui/minui.h" class GRSurfaceAdf : public GRSurface { - private: - int fence_fd; - int fd; - __u32 offset; - __u32 pitch; + public: + ~GRSurfaceAdf() override; + static std::unique_ptr<GRSurfaceAdf> Create(int intf_fd, const drm_mode_modeinfo* mode, + __u32 format, int* err); + + uint8_t* data() override { + return mmapped_buffer_; + } + + private: friend class MinuiBackendAdf; + + GRSurfaceAdf(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes, __u32 offset, + __u32 pitch, int fd) + : GRSurface(width, height, row_bytes, pixel_bytes), offset(offset), pitch(pitch), fd(fd) {} + + const __u32 offset; + const __u32 pitch; + + int fd; + int fence_fd{ -1 }; + uint8_t* mmapped_buffer_{ nullptr }; }; class MinuiBackendAdf : public MinuiBackend { public: + MinuiBackendAdf(); + ~MinuiBackendAdf() override; GRSurface* Init() override; GRSurface* Flip() override; void Blank(bool) override; - ~MinuiBackendAdf() override; - MinuiBackendAdf(); private: - int SurfaceInit(const drm_mode_modeinfo* mode, GRSurfaceAdf* surf); int InterfaceInit(); int DeviceInit(adf_device* dev); - void SurfaceDestroy(GRSurfaceAdf* surf); void Sync(GRSurfaceAdf* surf); int intf_fd; adf_id_t eng_id; __u32 format; adf_device dev; - unsigned int current_surface; - unsigned int n_surfaces; - GRSurfaceAdf surfaces[2]; + size_t current_surface; + size_t n_surfaces; + std::unique_ptr<GRSurfaceAdf> surfaces[2]; }; - -#endif // _GRAPHICS_ADF_H_ diff --git a/minui/graphics_drm.cpp b/minui/graphics_drm.cpp index 630b80180..7b2eed15d 100644 --- a/minui/graphics_drm.cpp +++ b/minui/graphics_drm.cpp @@ -24,74 +24,37 @@ #include <sys/types.h> #include <unistd.h> +#include <memory> + +#include <android-base/macros.h> +#include <android-base/stringprintf.h> +#include <android-base/unique_fd.h> #include <drm_fourcc.h> #include <xf86drm.h> #include <xf86drmMode.h> #include "minui/minui.h" -#define ARRAY_SIZE(A) (sizeof(A)/sizeof(*(A))) - -MinuiBackendDrm::MinuiBackendDrm() - : GRSurfaceDrms(), main_monitor_crtc(nullptr), main_monitor_connector(nullptr), drm_fd(-1) {} - -void MinuiBackendDrm::DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc) { - if (crtc) { - drmModeSetCrtc(drm_fd, crtc->crtc_id, - 0, // fb_id - 0, 0, // x,y - nullptr, // connectors - 0, // connector_count - nullptr); // mode - } -} - -int MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface) { - int ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0, // x,y - &main_monitor_connector->connector_id, - 1, // connector_count - &main_monitor_crtc->mode); - - if (ret) { - printf("drmModeSetCrtc failed ret=%d\n", ret); - } - - return ret; -} - -void MinuiBackendDrm::Blank(bool blank) { - if (blank) { - DrmDisableCrtc(drm_fd, main_monitor_crtc); - } else { - DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[current_buffer]); - } -} - -void MinuiBackendDrm::DrmDestroySurface(GRSurfaceDrm* surface) { - if (!surface) return; - - if (surface->data) { - munmap(surface->data, surface->row_bytes * surface->height); +GRSurfaceDrm::~GRSurfaceDrm() { + if (mmapped_buffer_) { + munmap(mmapped_buffer_, row_bytes * height); } - if (surface->fb_id) { - int ret = drmModeRmFB(drm_fd, surface->fb_id); - if (ret) { - printf("drmModeRmFB failed ret=%d\n", ret); + if (fb_id) { + if (drmModeRmFB(drm_fd_, fb_id) != 0) { + perror("Failed to drmModeRmFB"); + // Falling through to free other resources. } } - if (surface->handle) { + if (handle) { drm_gem_close gem_close = {}; - gem_close.handle = surface->handle; + gem_close.handle = handle; - int ret = drmIoctl(drm_fd, DRM_IOCTL_GEM_CLOSE, &gem_close); - if (ret) { - printf("DRM_IOCTL_GEM_CLOSE failed ret=%d\n", ret); + if (drmIoctl(drm_fd_, DRM_IOCTL_GEM_CLOSE, &gem_close) != 0) { + perror("Failed to DRM_IOCTL_GEM_CLOSE"); } } - - delete surface; } static int drm_format_to_bpp(uint32_t format) { @@ -111,10 +74,7 @@ static int drm_format_to_bpp(uint32_t format) { } } -GRSurfaceDrm* MinuiBackendDrm::DrmCreateSurface(int width, int height) { - GRSurfaceDrm* surface = new GRSurfaceDrm; - *surface = {}; - +std::unique_ptr<GRSurfaceDrm> GRSurfaceDrm::Create(int drm_fd, int width, int height) { uint32_t format; PixelFormat pixel_format = gr_pixel_format(); // PixelFormat comes in byte order, whereas DRM_FORMAT_* uses little-endian @@ -137,53 +97,74 @@ GRSurfaceDrm* MinuiBackendDrm::DrmCreateSurface(int width, int height) { create_dumb.bpp = drm_format_to_bpp(format); create_dumb.flags = 0; - int ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb); - if (ret) { - printf("DRM_IOCTL_MODE_CREATE_DUMB failed ret=%d\n", ret); - DrmDestroySurface(surface); + if (drmIoctl(drm_fd, DRM_IOCTL_MODE_CREATE_DUMB, &create_dumb) != 0) { + perror("Failed to DRM_IOCTL_MODE_CREATE_DUMB"); return nullptr; } - surface->handle = create_dumb.handle; + + // Cannot use std::make_unique to access non-public ctor. + auto surface = std::unique_ptr<GRSurfaceDrm>(new GRSurfaceDrm( + width, height, create_dumb.pitch, create_dumb.bpp / 8, drm_fd, create_dumb.handle)); uint32_t handles[4], pitches[4], offsets[4]; handles[0] = surface->handle; pitches[0] = create_dumb.pitch; offsets[0] = 0; - - ret = - drmModeAddFB2(drm_fd, width, height, format, handles, pitches, offsets, &(surface->fb_id), 0); - if (ret) { - printf("drmModeAddFB2 failed ret=%d\n", ret); - DrmDestroySurface(surface); + if (drmModeAddFB2(drm_fd, width, height, format, handles, pitches, offsets, &surface->fb_id, 0) != + 0) { + perror("Failed to drmModeAddFB2"); return nullptr; } drm_mode_map_dumb map_dumb = {}; map_dumb.handle = create_dumb.handle; - ret = drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb); - if (ret) { - printf("DRM_IOCTL_MODE_MAP_DUMB failed ret=%d\n", ret); - DrmDestroySurface(surface); + if (drmIoctl(drm_fd, DRM_IOCTL_MODE_MAP_DUMB, &map_dumb) != 0) { + perror("Failed to DRM_IOCTL_MODE_MAP_DUMB"); return nullptr; } - surface->height = height; - surface->width = width; - surface->row_bytes = create_dumb.pitch; - surface->pixel_bytes = create_dumb.bpp / 8; - surface->data = static_cast<unsigned char*>(mmap(nullptr, surface->height * surface->row_bytes, - PROT_READ | PROT_WRITE, MAP_SHARED, drm_fd, - map_dumb.offset)); - if (surface->data == MAP_FAILED) { - perror("mmap() failed"); - DrmDestroySurface(surface); + auto mmapped = mmap(nullptr, surface->height * surface->row_bytes, PROT_READ | PROT_WRITE, + MAP_SHARED, drm_fd, map_dumb.offset); + if (mmapped == MAP_FAILED) { + perror("Failed to mmap()"); return nullptr; } - + surface->mmapped_buffer_ = static_cast<uint8_t*>(mmapped); return surface; } +void MinuiBackendDrm::DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc) { + if (crtc) { + drmModeSetCrtc(drm_fd, crtc->crtc_id, + 0, // fb_id + 0, 0, // x,y + nullptr, // connectors + 0, // connector_count + nullptr); // mode + } +} + +bool MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, + const std::unique_ptr<GRSurfaceDrm>& surface) { + if (drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0, // x,y + &main_monitor_connector->connector_id, + 1, // connector_count + &main_monitor_crtc->mode) != 0) { + perror("Failed to drmModeSetCrtc"); + return false; + } + return true; +} + +void MinuiBackendDrm::Blank(bool blank) { + if (blank) { + DrmDisableCrtc(drm_fd, main_monitor_crtc); + } else { + DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[current_buffer]); + } +} + static drmModeCrtc* find_crtc_for_connector(int fd, drmModeRes* resources, drmModeConnector* connector) { // Find the encoder. If we already have one, just use it. @@ -265,7 +246,7 @@ drmModeConnector* MinuiBackendDrm::FindMainMonitor(int fd, drmModeRes* resources do { main_monitor_connector = find_used_connector_by_type(fd, resources, kConnectorPriority[i]); i++; - } while (!main_monitor_connector && i < ARRAY_SIZE(kConnectorPriority)); + } while (!main_monitor_connector && i < arraysize(kConnectorPriority)); /* If we didn't find a connector, grab the first one that is connected. */ if (!main_monitor_connector) { @@ -299,60 +280,53 @@ void MinuiBackendDrm::DisableNonMainCrtcs(int fd, drmModeRes* resources, drmMode GRSurface* MinuiBackendDrm::Init() { drmModeRes* res = nullptr; + drm_fd = -1; /* Consider DRM devices in order. */ for (int i = 0; i < DRM_MAX_MINOR; i++) { - char* dev_name; - int ret = asprintf(&dev_name, DRM_DEV_NAME, DRM_DIR_NAME, i); - if (ret < 0) continue; + auto dev_name = android::base::StringPrintf(DRM_DEV_NAME, DRM_DIR_NAME, i); + android::base::unique_fd fd(open(dev_name.c_str(), O_RDWR | O_CLOEXEC)); + if (fd == -1) continue; - drm_fd = open(dev_name, O_RDWR, 0); - free(dev_name); - if (drm_fd < 0) continue; - - uint64_t cap = 0; /* We need dumb buffers. */ - ret = drmGetCap(drm_fd, DRM_CAP_DUMB_BUFFER, &cap); - if (ret || cap == 0) { - close(drm_fd); + if (uint64_t cap = 0; drmGetCap(fd.get(), DRM_CAP_DUMB_BUFFER, &cap) != 0 || cap == 0) { continue; } - res = drmModeGetResources(drm_fd); + res = drmModeGetResources(fd.get()); if (!res) { - close(drm_fd); continue; } /* Use this device if it has at least one connected monitor. */ if (res->count_crtcs > 0 && res->count_connectors > 0) { - if (find_first_connected_connector(drm_fd, res)) break; + if (find_first_connected_connector(fd.get(), res)) { + drm_fd = fd.release(); + break; + } } drmModeFreeResources(res); - close(drm_fd); res = nullptr; } - if (drm_fd < 0 || res == nullptr) { - perror("cannot find/open a drm device"); + if (drm_fd == -1 || res == nullptr) { + perror("Failed to find/open a drm device"); return nullptr; } uint32_t selected_mode; main_monitor_connector = FindMainMonitor(drm_fd, res, &selected_mode); - if (!main_monitor_connector) { - printf("main_monitor_connector not found\n"); + fprintf(stderr, "Failed to find main_monitor_connector\n"); drmModeFreeResources(res); close(drm_fd); return nullptr; } main_monitor_crtc = find_crtc_for_connector(drm_fd, res, main_monitor_connector); - if (!main_monitor_crtc) { - printf("main_monitor_crtc not found\n"); + fprintf(stderr, "Failed to find main_monitor_crtc\n"); drmModeFreeResources(res); close(drm_fd); return nullptr; @@ -367,21 +341,20 @@ GRSurface* MinuiBackendDrm::Init() { drmModeFreeResources(res); - GRSurfaceDrms[0] = DrmCreateSurface(width, height); - GRSurfaceDrms[1] = DrmCreateSurface(width, height); + GRSurfaceDrms[0] = GRSurfaceDrm::Create(drm_fd, width, height); + GRSurfaceDrms[1] = GRSurfaceDrm::Create(drm_fd, width, height); if (!GRSurfaceDrms[0] || !GRSurfaceDrms[1]) { - // GRSurfaceDrms and drm_fd should be freed in d'tor. return nullptr; } current_buffer = 0; // We will likely encounter errors in the backend functions (i.e. Flip) if EnableCrtc fails. - if (DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]) != 0) { + if (!DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1])) { return nullptr; } - return GRSurfaceDrms[0]; + return GRSurfaceDrms[0].get(); } static void page_flip_complete(__unused int fd, @@ -394,12 +367,9 @@ static void page_flip_complete(__unused int fd, GRSurface* MinuiBackendDrm::Flip() { bool ongoing_flip = true; - - int ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id, - GRSurfaceDrms[current_buffer]->fb_id, - DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip); - if (ret < 0) { - printf("drmModePageFlip failed ret=%d\n", ret); + if (drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id, GRSurfaceDrms[current_buffer]->fb_id, + DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip) != 0) { + perror("Failed to drmModePageFlip"); return nullptr; } @@ -409,9 +379,8 @@ GRSurface* MinuiBackendDrm::Flip() { .events = POLLIN }; - ret = poll(&fds, 1, -1); - if (ret == -1 || !(fds.revents & POLLIN)) { - printf("poll() failed on drm fd\n"); + if (poll(&fds, 1, -1) == -1 || !(fds.revents & POLLIN)) { + perror("Failed to poll() on drm fd"); break; } @@ -420,21 +389,18 @@ GRSurface* MinuiBackendDrm::Flip() { .page_flip_handler = page_flip_complete }; - ret = drmHandleEvent(drm_fd, &evctx); - if (ret != 0) { - printf("drmHandleEvent failed ret=%d\n", ret); + if (drmHandleEvent(drm_fd, &evctx) != 0) { + perror("Failed to drmHandleEvent"); break; } } current_buffer = 1 - current_buffer; - return GRSurfaceDrms[current_buffer]; + return GRSurfaceDrms[current_buffer].get(); } MinuiBackendDrm::~MinuiBackendDrm() { DrmDisableCrtc(drm_fd, main_monitor_crtc); - DrmDestroySurface(GRSurfaceDrms[0]); - DrmDestroySurface(GRSurfaceDrms[1]); drmModeFreeCrtc(main_monitor_crtc); drmModeFreeConnector(main_monitor_connector); close(drm_fd); diff --git a/minui/graphics_drm.h b/minui/graphics_drm.h index 756625b03..57ba39b83 100644 --- a/minui/graphics_drm.h +++ b/minui/graphics_drm.h @@ -14,45 +14,61 @@ * limitations under the License. */ -#ifndef _GRAPHICS_DRM_H_ -#define _GRAPHICS_DRM_H_ +#pragma once +#include <stddef.h> #include <stdint.h> +#include <memory> + #include <xf86drmMode.h> #include "graphics.h" #include "minui/minui.h" class GRSurfaceDrm : public GRSurface { - private: - uint32_t fb_id; - uint32_t handle; + public: + ~GRSurfaceDrm() override; + + // Creates a GRSurfaceDrm instance. + static std::unique_ptr<GRSurfaceDrm> Create(int drm_fd, int width, int height); + uint8_t* data() override { + return mmapped_buffer_; + } + + private: friend class MinuiBackendDrm; + + GRSurfaceDrm(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes, int drm_fd, + uint32_t handle) + : GRSurface(width, height, row_bytes, pixel_bytes), drm_fd_(drm_fd), handle(handle) {} + + const int drm_fd_; + + uint32_t fb_id{ 0 }; + uint32_t handle{ 0 }; + uint8_t* mmapped_buffer_{ nullptr }; }; class MinuiBackendDrm : public MinuiBackend { public: + MinuiBackendDrm() = default; + ~MinuiBackendDrm() override; + GRSurface* Init() override; GRSurface* Flip() override; void Blank(bool) override; - ~MinuiBackendDrm() override; - MinuiBackendDrm(); private: void DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc); - int DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface); - GRSurfaceDrm* DrmCreateSurface(int width, int height); - void DrmDestroySurface(GRSurfaceDrm* surface); + bool DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, const std::unique_ptr<GRSurfaceDrm>& surface); void DisableNonMainCrtcs(int fd, drmModeRes* resources, drmModeCrtc* main_crtc); drmModeConnector* FindMainMonitor(int fd, drmModeRes* resources, uint32_t* mode_index); - GRSurfaceDrm* GRSurfaceDrms[2]; - int current_buffer; - drmModeCrtc* main_monitor_crtc; - drmModeConnector* main_monitor_connector; - int drm_fd; + std::unique_ptr<GRSurfaceDrm> GRSurfaceDrms[2]; + int current_buffer{ 0 }; + drmModeCrtc* main_monitor_crtc{ nullptr }; + drmModeConnector* main_monitor_connector{ nullptr }; + int drm_fd{ -1 }; }; - -#endif // _GRAPHICS_DRM_H_ diff --git a/minui/graphics_fbdev.cpp b/minui/graphics_fbdev.cpp index 746f42aaa..2584017d6 100644 --- a/minui/graphics_fbdev.cpp +++ b/minui/graphics_fbdev.cpp @@ -26,21 +26,29 @@ #include <sys/types.h> #include <unistd.h> +#include <memory> + +#include <android-base/unique_fd.h> + #include "minui/minui.h" -MinuiBackendFbdev::MinuiBackendFbdev() : gr_draw(nullptr), fb_fd(-1) {} +std::unique_ptr<GRSurfaceFbdev> GRSurfaceFbdev::Create(size_t width, size_t height, + size_t row_bytes, size_t pixel_bytes) { + // Cannot use std::make_unique to access non-public ctor. + return std::unique_ptr<GRSurfaceFbdev>(new GRSurfaceFbdev(width, height, row_bytes, pixel_bytes)); +} void MinuiBackendFbdev::Blank(bool blank) { int ret = ioctl(fb_fd, FBIOBLANK, blank ? FB_BLANK_POWERDOWN : FB_BLANK_UNBLANK); if (ret < 0) perror("ioctl(): blank"); } -void MinuiBackendFbdev::SetDisplayedFramebuffer(unsigned n) { +void MinuiBackendFbdev::SetDisplayedFramebuffer(size_t n) { if (n > 1 || !double_buffered) return; - vi.yres_virtual = gr_framebuffer[0].height * 2; - vi.yoffset = n * gr_framebuffer[0].height; - vi.bits_per_pixel = gr_framebuffer[0].pixel_bytes * 8; + vi.yres_virtual = gr_framebuffer[0]->height * 2; + vi.yoffset = n * gr_framebuffer[0]->height; + vi.bits_per_pixel = gr_framebuffer[0]->pixel_bytes * 8; if (ioctl(fb_fd, FBIOPUT_VSCREENINFO, &vi) < 0) { perror("active fb swap failed"); } @@ -48,7 +56,7 @@ void MinuiBackendFbdev::SetDisplayedFramebuffer(unsigned n) { } GRSurface* MinuiBackendFbdev::Init() { - int fd = open("/dev/graphics/fb0", O_RDWR); + android::base::unique_fd fd(open("/dev/graphics/fb0", O_RDWR | O_CLOEXEC)); if (fd == -1) { perror("cannot open fb0"); return nullptr; @@ -57,13 +65,11 @@ GRSurface* MinuiBackendFbdev::Init() { fb_fix_screeninfo fi; if (ioctl(fd, FBIOGET_FSCREENINFO, &fi) < 0) { perror("failed to get fb0 info"); - close(fd); return nullptr; } if (ioctl(fd, FBIOGET_VSCREENINFO, &vi) < 0) { perror("failed to get fb0 info"); - close(fd); return nullptr; } @@ -90,50 +96,41 @@ GRSurface* MinuiBackendFbdev::Init() { void* bits = mmap(0, fi.smem_len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (bits == MAP_FAILED) { perror("failed to mmap framebuffer"); - close(fd); return nullptr; } memset(bits, 0, fi.smem_len); - gr_framebuffer[0].width = vi.xres; - gr_framebuffer[0].height = vi.yres; - gr_framebuffer[0].row_bytes = fi.line_length; - gr_framebuffer[0].pixel_bytes = vi.bits_per_pixel / 8; - gr_framebuffer[0].data = static_cast<uint8_t*>(bits); - memset(gr_framebuffer[0].data, 0, gr_framebuffer[0].height * gr_framebuffer[0].row_bytes); + gr_framebuffer[0] = + GRSurfaceFbdev::Create(vi.xres, vi.yres, fi.line_length, vi.bits_per_pixel / 8); + gr_framebuffer[0]->buffer_ = static_cast<uint8_t*>(bits); + memset(gr_framebuffer[0]->buffer_, 0, gr_framebuffer[0]->height * gr_framebuffer[0]->row_bytes); + + gr_framebuffer[1] = + GRSurfaceFbdev::Create(gr_framebuffer[0]->width, gr_framebuffer[0]->height, + gr_framebuffer[0]->row_bytes, gr_framebuffer[0]->pixel_bytes); /* check if we can use double buffering */ if (vi.yres * fi.line_length * 2 <= fi.smem_len) { double_buffered = true; - memcpy(gr_framebuffer + 1, gr_framebuffer, sizeof(GRSurface)); - gr_framebuffer[1].data = - gr_framebuffer[0].data + gr_framebuffer[0].height * gr_framebuffer[0].row_bytes; - - gr_draw = gr_framebuffer + 1; - + gr_framebuffer[1]->buffer_ = + gr_framebuffer[0]->buffer_ + gr_framebuffer[0]->height * gr_framebuffer[0]->row_bytes; } else { double_buffered = false; - // Without double-buffering, we allocate RAM for a buffer to - // draw in, and then "flipping" the buffer consists of a - // memcpy from the buffer we allocated to the framebuffer. - - gr_draw = static_cast<GRSurface*>(malloc(sizeof(GRSurface))); - memcpy(gr_draw, gr_framebuffer, sizeof(GRSurface)); - gr_draw->data = static_cast<unsigned char*>(malloc(gr_draw->height * gr_draw->row_bytes)); - if (!gr_draw->data) { - perror("failed to allocate in-memory surface"); - return nullptr; - } + // Without double-buffering, we allocate RAM for a buffer to draw in, and then "flipping" the + // buffer consists of a memcpy from the buffer we allocated to the framebuffer. + memory_buffer.resize(gr_framebuffer[1]->height * gr_framebuffer[1]->row_bytes); + gr_framebuffer[1]->buffer_ = memory_buffer.data(); } - memset(gr_draw->data, 0, gr_draw->height * gr_draw->row_bytes); - fb_fd = fd; + gr_draw = gr_framebuffer[1].get(); + memset(gr_draw->buffer_, 0, gr_draw->height * gr_draw->row_bytes); + fb_fd = std::move(fd); SetDisplayedFramebuffer(0); - printf("framebuffer: %d (%d x %d)\n", fb_fd, gr_draw->width, gr_draw->height); + printf("framebuffer: %d (%zu x %zu)\n", fb_fd.get(), gr_draw->width, gr_draw->height); Blank(true); Blank(false); @@ -143,25 +140,13 @@ GRSurface* MinuiBackendFbdev::Init() { GRSurface* MinuiBackendFbdev::Flip() { if (double_buffered) { - // Change gr_draw to point to the buffer currently displayed, - // then flip the driver so we're displaying the other buffer - // instead. - gr_draw = gr_framebuffer + displayed_buffer; + // Change gr_draw to point to the buffer currently displayed, then flip the driver so we're + // displaying the other buffer instead. + gr_draw = gr_framebuffer[displayed_buffer].get(); SetDisplayedFramebuffer(1 - displayed_buffer); } else { // Copy from the in-memory surface to the framebuffer. - memcpy(gr_framebuffer[0].data, gr_draw->data, gr_draw->height * gr_draw->row_bytes); + memcpy(gr_framebuffer[0]->buffer_, gr_draw->buffer_, gr_draw->height * gr_draw->row_bytes); } return gr_draw; } - -MinuiBackendFbdev::~MinuiBackendFbdev() { - close(fb_fd); - fb_fd = -1; - - if (!double_buffered && gr_draw) { - free(gr_draw->data); - free(gr_draw); - } - gr_draw = nullptr; -} diff --git a/minui/graphics_fbdev.h b/minui/graphics_fbdev.h index 107e19567..596ba74ea 100644 --- a/minui/graphics_fbdev.h +++ b/minui/graphics_fbdev.h @@ -14,31 +14,58 @@ * limitations under the License. */ -#ifndef _GRAPHICS_FBDEV_H_ -#define _GRAPHICS_FBDEV_H_ +#pragma once #include <linux/fb.h> +#include <stddef.h> +#include <stdint.h> + +#include <memory> +#include <vector> + +#include <android-base/unique_fd.h> #include "graphics.h" #include "minui/minui.h" +class GRSurfaceFbdev : public GRSurface { + public: + // Creates and returns a GRSurfaceFbdev instance, or nullptr on error. + static std::unique_ptr<GRSurfaceFbdev> Create(size_t width, size_t height, size_t row_bytes, + size_t pixel_bytes); + + uint8_t* data() override { + return buffer_; + } + + protected: + using GRSurface::GRSurface; + + private: + friend class MinuiBackendFbdev; + + // Points to the start of the buffer: either the mmap'd framebuffer or one allocated in-memory. + uint8_t* buffer_{ nullptr }; +}; + class MinuiBackendFbdev : public MinuiBackend { public: + MinuiBackendFbdev() = default; + ~MinuiBackendFbdev() override = default; + GRSurface* Init() override; GRSurface* Flip() override; void Blank(bool) override; - ~MinuiBackendFbdev() override; - MinuiBackendFbdev(); private: - void SetDisplayedFramebuffer(unsigned n); + void SetDisplayedFramebuffer(size_t n); - GRSurface gr_framebuffer[2]; + std::unique_ptr<GRSurfaceFbdev> gr_framebuffer[2]; + // Points to the current surface (i.e. one of the two gr_framebuffer's). + GRSurfaceFbdev* gr_draw{ nullptr }; bool double_buffered; - GRSurface* gr_draw; - int displayed_buffer; + std::vector<uint8_t> memory_buffer; + size_t displayed_buffer{ 0 }; fb_var_screeninfo vi; - int fb_fd; + android::base::unique_fd fb_fd; }; - -#endif // _GRAPHICS_FBDEV_H_ diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h index fa13ecdff..36bdcf103 100644 --- a/minui/include/minui/minui.h +++ b/minui/include/minui/minui.h @@ -14,25 +14,73 @@ * limitations under the License. */ -#ifndef _MINUI_H_ -#define _MINUI_H_ +#pragma once +#include <stdint.h> +#include <stdlib.h> #include <sys/types.h> #include <functional> +#include <memory> #include <string> #include <vector> +#include <android-base/macros.h> +#include <android-base/unique_fd.h> + // // Graphics. // -struct GRSurface { - int width; - int height; - int row_bytes; - int pixel_bytes; - unsigned char* data; +class GRSurface { + public: + static constexpr size_t kSurfaceDataAlignment = 8; + + virtual ~GRSurface() = default; + + // Creates and returns a GRSurface instance that's sufficient for storing an image of the given + // size (i.e. row_bytes * height). The starting address of the surface data is aligned to + // kSurfaceDataAlignment. Returns the created GRSurface instance (in std::unique_ptr), or nullptr + // on error. + static std::unique_ptr<GRSurface> Create(size_t width, size_t height, size_t row_bytes, + size_t pixel_bytes); + + // Clones the current GRSurface instance (i.e. an image). + std::unique_ptr<GRSurface> Clone() const; + + virtual uint8_t* data() { + return data_.get(); + } + + const uint8_t* data() const { + return const_cast<const uint8_t*>(const_cast<GRSurface*>(this)->data()); + } + + size_t data_size() const { + return data_size_; + } + + size_t width; + size_t height; + size_t row_bytes; + size_t pixel_bytes; + + protected: + GRSurface(size_t width, size_t height, size_t row_bytes, size_t pixel_bytes) + : width(width), height(height), row_bytes(row_bytes), pixel_bytes(pixel_bytes) {} + + private: + // The deleter for data_, whose data is allocated via aligned_alloc(3). + struct DataDeleter { + void operator()(uint8_t* data) { + free(data); + } + }; + + std::unique_ptr<uint8_t, DataDeleter> data_; + size_t data_size_; + + DISALLOW_COPY_AND_ASSIGN(GRSurface); }; struct GRFont { @@ -75,7 +123,7 @@ void gr_clear(); void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a); void gr_fill(int x1, int y1, int x2, int y2); -void gr_texticon(int x, int y, GRSurface* icon); +void gr_texticon(int x, int y, const GRSurface* icon); const GRFont* gr_sys_font(); int gr_init_font(const char* name, GRFont** dest); @@ -85,7 +133,7 @@ int gr_measure(const GRFont* font, const char* s); // Returns -1 if font is nullptr. int gr_font_size(const GRFont* font, int* x, int* y); -void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy); +void gr_blit(const GRSurface* source, int sx, int sy, int w, int h, int dx, int dy); unsigned int gr_get_width(const GRSurface* surface); unsigned int gr_get_height(const GRSurface* surface); @@ -106,7 +154,7 @@ using ev_set_key_callback = std::function<int(int code, int value)>; int ev_init(ev_callback input_cb, bool allow_touch_inputs = false); void ev_exit(); -int ev_add_fd(int fd, ev_callback cb); +int ev_add_fd(android::base::unique_fd&& fd, ev_callback cb); void ev_iterate_available_keys(const std::function<void(int)>& f); void ev_iterate_touch_inputs(const std::function<void(int)>& action); int ev_sync_key_state(const ev_set_key_callback& set_key_cb); @@ -165,5 +213,3 @@ std::vector<std::string> get_locales_in_png(const std::string& png_name); // Free a surface allocated by any of the res_create_*_surface() // functions. void res_free_surface(GRSurface* surface); - -#endif diff --git a/minui/resources.cpp b/minui/resources.cpp index 477fbe2a2..069a49529 100644 --- a/minui/resources.cpp +++ b/minui/resources.cpp @@ -27,6 +27,7 @@ #include <sys/types.h> #include <unistd.h> +#include <limits> #include <memory> #include <regex> #include <string> @@ -37,18 +38,29 @@ #include "minui/minui.h" -#define SURFACE_DATA_ALIGNMENT 8 - static std::string g_resource_dir{ "/res/images" }; -static GRSurface* malloc_surface(size_t data_size) { - size_t size = sizeof(GRSurface) + data_size + SURFACE_DATA_ALIGNMENT; - unsigned char* temp = static_cast<unsigned char*>(malloc(size)); - if (temp == NULL) return NULL; - GRSurface* surface = reinterpret_cast<GRSurface*>(temp); - surface->data = temp + sizeof(GRSurface) + - (SURFACE_DATA_ALIGNMENT - (sizeof(GRSurface) % SURFACE_DATA_ALIGNMENT)); - return surface; +std::unique_ptr<GRSurface> GRSurface::Create(size_t width, size_t height, size_t row_bytes, + size_t pixel_bytes) { + if (width == 0 || row_bytes == 0 || height == 0 || pixel_bytes == 0) return nullptr; + if (std::numeric_limits<size_t>::max() / row_bytes < height) return nullptr; + + // Cannot use std::make_unique to access non-public ctor. + auto result = std::unique_ptr<GRSurface>(new GRSurface(width, height, row_bytes, pixel_bytes)); + size_t data_size = row_bytes * height; + result->data_size_ = + (data_size + kSurfaceDataAlignment - 1) / kSurfaceDataAlignment * kSurfaceDataAlignment; + result->data_.reset( + static_cast<uint8_t*>(aligned_alloc(kSurfaceDataAlignment, result->data_size_))); + if (!result->data_) return nullptr; + return result; +} + +std::unique_ptr<GRSurface> GRSurface::Clone() const { + auto result = GRSurface::Create(width, height, row_bytes, pixel_bytes); + if (!result) return nullptr; + memcpy(result->data(), data(), data_size_); + return result; } PngHandler::PngHandler(const std::string& name) { @@ -63,7 +75,7 @@ PngHandler::PngHandler(const std::string& name) { return; } - unsigned char header[8]; + uint8_t header[8]; size_t bytesRead = fread(header, 1, sizeof(header), png_fp_.get()); if (bytesRead != sizeof(header)) { error_code_ = -2; @@ -126,70 +138,49 @@ PngHandler::~PngHandler() { } } -// "display" surfaces are transformed into the framebuffer's required -// pixel format (currently only RGBX is supported) at load time, so -// gr_blit() can be nothing more than a memcpy() for each row. The -// next two functions are the only ones that know anything about the -// framebuffer pixel format; they need to be modified if the -// framebuffer format changes (but nothing else should). - -// Allocate and return a GRSurface* sufficient for storing an image of -// the indicated size in the framebuffer pixel format. -static GRSurface* init_display_surface(png_uint_32 width, png_uint_32 height) { - GRSurface* surface = malloc_surface(width * height * 4); - if (surface == NULL) return NULL; - - surface->width = width; - surface->height = height; - surface->row_bytes = width * 4; - surface->pixel_bytes = 4; - - return surface; -} +// "display" surfaces are transformed into the framebuffer's required pixel format (currently only +// RGBX is supported) at load time, so gr_blit() can be nothing more than a memcpy() for each row. -// Copy 'input_row' to 'output_row', transforming it to the -// framebuffer pixel format. The input format depends on the value of -// 'channels': +// Copies 'input_row' to 'output_row', transforming it to the framebuffer pixel format. The input +// format depends on the value of 'channels': // // 1 - input is 8-bit grayscale // 3 - input is 24-bit RGB // 4 - input is 32-bit RGBA/RGBX // // 'width' is the number of pixels in the row. -static void transform_rgb_to_draw(unsigned char* input_row, - unsigned char* output_row, - int channels, int width) { - int x; - unsigned char* ip = input_row; - unsigned char* op = output_row; - - switch (channels) { - case 1: - // expand gray level to RGBX - for (x = 0; x < width; ++x) { - *op++ = *ip; - *op++ = *ip; - *op++ = *ip; - *op++ = 0xff; - ip++; - } - break; - - case 3: - // expand RGBA to RGBX - for (x = 0; x < width; ++x) { - *op++ = *ip++; - *op++ = *ip++; - *op++ = *ip++; - *op++ = 0xff; - } - break; - - case 4: - // copy RGBA to RGBX - memcpy(output_row, input_row, width*4); - break; - } +static void TransformRgbToDraw(const uint8_t* input_row, uint8_t* output_row, int channels, + int width) { + const uint8_t* ip = input_row; + uint8_t* op = output_row; + + switch (channels) { + case 1: + // expand gray level to RGBX + for (int x = 0; x < width; ++x) { + *op++ = *ip; + *op++ = *ip; + *op++ = *ip; + *op++ = 0xff; + ip++; + } + break; + + case 3: + // expand RGBA to RGBX + for (int x = 0; x < width; ++x) { + *op++ = *ip++; + *op++ = *ip++; + *op++ = *ip++; + *op++ = 0xff; + } + break; + + case 4: + // copy RGBA to RGBX + memcpy(output_row, input_row, width * 4); + break; + } } int res_create_display_surface(const char* name, GRSurface** pSurface) { @@ -202,7 +193,7 @@ int res_create_display_surface(const char* name, GRSurface** pSurface) { png_uint_32 width = png_handler.width(); png_uint_32 height = png_handler.height(); - GRSurface* surface = init_display_surface(width, height); + auto surface = GRSurface::Create(width, height, width * 4, 4); if (!surface) { return -8; } @@ -213,13 +204,13 @@ int res_create_display_surface(const char* name, GRSurface** pSurface) { } for (png_uint_32 y = 0; y < height; ++y) { - std::vector<unsigned char> p_row(width * 4); + std::vector<uint8_t> p_row(width * 4); png_read_row(png_ptr, p_row.data(), nullptr); - transform_rgb_to_draw(p_row.data(), surface->data + y * surface->row_bytes, - png_handler.channels(), width); + TransformRgbToDraw(p_row.data(), surface->data() + y * surface->row_bytes, + png_handler.channels(), width); } - *pSurface = surface; + *pSurface = surface.release(); return 0; } @@ -272,11 +263,12 @@ int res_create_multi_display_surface(const char* name, int* frames, int* fps, goto exit; } for (int i = 0; i < *frames; ++i) { - surface[i] = init_display_surface(width, height / *frames); - if (!surface[i]) { + auto created_surface = GRSurface::Create(width, height / *frames, width * 4, 4); + if (!created_surface) { result = -8; goto exit; } + surface[i] = created_surface.release(); } if (gr_pixel_format() == PixelFormat::ABGR || gr_pixel_format() == PixelFormat::BGRA) { @@ -284,11 +276,11 @@ int res_create_multi_display_surface(const char* name, int* frames, int* fps, } for (png_uint_32 y = 0; y < height; ++y) { - std::vector<unsigned char> p_row(width * 4); + std::vector<uint8_t> p_row(width * 4); png_read_row(png_ptr, p_row.data(), nullptr); int frame = y % *frames; - unsigned char* out_row = surface[frame]->data + (y / *frames) * surface[frame]->row_bytes; - transform_rgb_to_draw(p_row.data(), out_row, png_handler.channels(), width); + uint8_t* out_row = surface[frame]->data() + (y / *frames) * surface[frame]->row_bytes; + TransformRgbToDraw(p_row.data(), out_row, png_handler.channels(), width); } *pSurface = surface; @@ -319,14 +311,10 @@ int res_create_alpha_surface(const char* name, GRSurface** pSurface) { png_uint_32 width = png_handler.width(); png_uint_32 height = png_handler.height(); - GRSurface* surface = malloc_surface(width * height); + auto surface = GRSurface::Create(width, height, width, 1); if (!surface) { return -8; } - surface->width = width; - surface->height = height; - surface->row_bytes = width; - surface->pixel_bytes = 1; PixelFormat pixel_format = gr_pixel_format(); if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) { @@ -334,11 +322,11 @@ int res_create_alpha_surface(const char* name, GRSurface** pSurface) { } for (png_uint_32 y = 0; y < height; ++y) { - unsigned char* p_row = surface->data + y * surface->row_bytes; + uint8_t* p_row = surface->data() + y * surface->row_bytes; png_read_row(png_ptr, p_row, nullptr); } - *pSurface = surface; + *pSurface = surface.release(); return 0; } @@ -383,7 +371,7 @@ std::vector<std::string> get_locales_in_png(const std::string& png_name) { } std::vector<std::string> result; - std::vector<unsigned char> row(png_handler.width()); + std::vector<uint8_t> row(png_handler.width()); for (png_uint_32 y = 0; y < png_handler.height(); ++y) { png_read_row(png_handler.png_ptr(), row.data(), nullptr); int h = (row[3] << 8) | row[2]; @@ -419,7 +407,7 @@ int res_create_localized_alpha_surface(const char* name, png_uint_32 height = png_handler.height(); for (png_uint_32 y = 0; y < height; ++y) { - std::vector<unsigned char> row(width); + std::vector<uint8_t> row(width); png_read_row(png_ptr, row.data(), nullptr); int w = (row[1] << 8) | row[0]; int h = (row[3] << 8) | row[2]; @@ -429,21 +417,17 @@ int res_create_localized_alpha_surface(const char* name, if (y + 1 + h >= height || matches_locale(loc, locale)) { printf(" %20s: %s (%d x %d @ %d)\n", name, loc, w, h, y); - GRSurface* surface = malloc_surface(w * h); + auto surface = GRSurface::Create(w, h, w, 1); if (!surface) { return -8; } - surface->width = w; - surface->height = h; - surface->row_bytes = w; - surface->pixel_bytes = 1; for (int i = 0; i < h; ++i, ++y) { png_read_row(png_ptr, row.data(), nullptr); - memcpy(surface->data + i * w, row.data(), w); + memcpy(surface->data() + i * w, row.data(), w); } - *pSurface = surface; + *pSurface = surface.release(); break; } diff --git a/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/package.cpp b/package.cpp new file mode 100644 index 000000000..d40427859 --- /dev/null +++ b/package.cpp @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "package.h" + +#include <string.h> + +#include <android-base/logging.h> +#include <android-base/stringprintf.h> +#include <openssl/sha.h> + +#include "otautil/error_code.h" +#include "otautil/sysutil.h" + +// This class wraps the package in memory, i.e. a memory mapped package, or a package loaded +// to a string/vector. +class MemoryPackage : public Package { + public: + // Constructs the class from a file. We will memory maps the file later. + MemoryPackage(const std::string& path, std::unique_ptr<MemMapping> map, + const std::function<void(float)>& set_progress); + + // Constructs the class from the package bytes in |content|. + MemoryPackage(std::vector<uint8_t> content, const std::function<void(float)>& set_progress); + + ~MemoryPackage() override; + + // Memory maps the package file if necessary. Initializes the start address and size of the + // package. + uint64_t GetPackageSize() const override { + return package_size_; + } + + bool ReadFullyAtOffset(uint8_t* buffer, uint64_t byte_count, uint64_t offset) override; + + ZipArchiveHandle GetZipArchiveHandle() override; + + bool UpdateHashAtOffset(const std::vector<HasherUpdateCallback>& hashers, uint64_t start, + uint64_t length) override; + + private: + const uint8_t* addr_; // Start address of the package in memory. + uint64_t package_size_; // Package size in bytes. + + // The memory mapped package. + std::unique_ptr<MemMapping> map_; + // A copy of the package content, valid only if we create the class with the exact bytes of + // the package. + std::vector<uint8_t> package_content_; + // The physical path to the package, empty if we create the class with the package content. + std::string path_; + + // The ZipArchiveHandle of the package. + ZipArchiveHandle zip_handle_; +}; + +// TODO(xunchang) Implement the PackageFromFd. + +void Package::SetProgress(float progress) { + if (set_progress_) { + set_progress_(progress); + } +} + +std::unique_ptr<Package> Package::CreateMemoryPackage( + const std::string& path, const std::function<void(float)>& set_progress) { + std::unique_ptr<MemMapping> mmap = std::make_unique<MemMapping>(); + if (!mmap->MapFile(path)) { + LOG(ERROR) << "failed to map file"; + return nullptr; + } + + return std::make_unique<MemoryPackage>(path, std::move(mmap), set_progress); +} + +std::unique_ptr<Package> Package::CreateMemoryPackage( + std::vector<uint8_t> content, const std::function<void(float)>& set_progress) { + return std::make_unique<MemoryPackage>(std::move(content), set_progress); +} + +MemoryPackage::MemoryPackage(const std::string& path, std::unique_ptr<MemMapping> map, + const std::function<void(float)>& set_progress) + : map_(std::move(map)), path_(path), zip_handle_(nullptr) { + addr_ = map_->addr; + package_size_ = map_->length; + set_progress_ = set_progress; +} + +MemoryPackage::MemoryPackage(std::vector<uint8_t> content, + const std::function<void(float)>& set_progress) + : package_content_(std::move(content)), zip_handle_(nullptr) { + CHECK(!package_content_.empty()); + addr_ = package_content_.data(); + package_size_ = package_content_.size(); + set_progress_ = set_progress; +} + +MemoryPackage::~MemoryPackage() { + if (zip_handle_) { + CloseArchive(zip_handle_); + } +} + +bool MemoryPackage::ReadFullyAtOffset(uint8_t* buffer, uint64_t byte_count, uint64_t offset) { + if (byte_count > package_size_ || offset > package_size_ - byte_count) { + LOG(ERROR) << "Out of bound read, offset: " << offset << ", size: " << byte_count + << ", total package_size: " << package_size_; + return false; + } + memcpy(buffer, addr_ + offset, byte_count); + return true; +} + +bool MemoryPackage::UpdateHashAtOffset(const std::vector<HasherUpdateCallback>& hashers, + uint64_t start, uint64_t length) { + if (length > package_size_ || start > package_size_ - length) { + LOG(ERROR) << "Out of bound read, offset: " << start << ", size: " << length + << ", total package_size: " << package_size_; + return false; + } + + for (const auto& hasher : hashers) { + hasher(addr_ + start, length); + } + return true; +} + +ZipArchiveHandle MemoryPackage::GetZipArchiveHandle() { + if (zip_handle_) { + return zip_handle_; + } + + if (auto err = OpenArchiveFromMemory(const_cast<uint8_t*>(addr_), package_size_, path_.c_str(), + &zip_handle_); + err != 0) { + LOG(ERROR) << "Can't open package" << path_ << " : " << ErrorCodeString(err); + return nullptr; + } + + return zip_handle_; +} diff --git a/package.h b/package.h new file mode 100644 index 000000000..5eef9c965 --- /dev/null +++ b/package.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <stdint.h> + +#include <functional> +#include <map> +#include <memory> +#include <string> +#include <vector> + +#include <ziparchive/zip_archive.h> + +#include "verifier.h" + +// This class serves as a wrapper for an OTA update package. It aims to provide the common +// interface for both packages loaded in memory and packages read from fd. +class Package : public VerifierInterface { + public: + virtual ~Package() = default; + + // Opens the package as a zip file and returns the ZipArchiveHandle. + virtual ZipArchiveHandle GetZipArchiveHandle() = 0; + + // Updates the progress in fraction during package verification. + void SetProgress(float progress) override; + + static std::unique_ptr<Package> CreateMemoryPackage( + const std::string& path, const std::function<void(float)>& set_progress); + + static std::unique_ptr<Package> CreateMemoryPackage( + std::vector<uint8_t> content, const std::function<void(float)>& set_progress); + + protected: + // An optional function to update the progress. + std::function<void(float)> set_progress_; +}; diff --git a/recovery-persist.cpp b/recovery-persist.cpp index d3ade6260..e2a6699f6 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 (access(LAST_INSTALL_FILE_IN_CACHE, F_OK) && unlink(LAST_INSTALL_FILE_IN_CACHE) == -1) { + PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE_IN_CACHE; + } + + // TBD: Future location to move content from /cache/recovery to /data/misc/recovery/ + // if --force-persist flag, then transfer pmsg data anyways + if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) { + return 0; + } } /* Is there something in pmsg? */ @@ -157,6 +178,15 @@ 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 (access(LAST_INSTALL_FILE, F_OK) && unlink(LAST_INSTALL_FILE) == -1) { + PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE; + } + } + /* Is there a last console log too? */ if (rotated) { if (!access(LAST_CONSOLE_FILE, R_OK)) { diff --git a/recovery.cpp b/recovery.cpp index 5934b61d7..0074b6433 100644 --- a/recovery.cpp +++ b/recovery.cpp @@ -64,6 +64,7 @@ #include "otautil/error_code.h" #include "otautil/paths.h" #include "otautil/sysutil.h" +#include "package.h" #include "roots.h" #include "screen_ui.h" #include "ui.h" @@ -294,7 +295,7 @@ bool SetUsbConfig(const std::string& state) { // Returns the selected filename, or an empty string. static std::string browse_directory(const std::string& path, Device* device) { - ensure_path_mounted(path.c_str()); + ensure_path_mounted(path); std::unique_ptr<DIR, decltype(&closedir)> d(opendir(path.c_str()), closedir); if (!d) { @@ -369,7 +370,14 @@ static bool yes_no(Device* device, const char* question1, const char* question2) } static bool ask_to_wipe_data(Device* device) { - return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!"); + std::vector<std::string> headers{ "Wipe all user data?", " THIS CAN NOT BE UNDONE!" }; + std::vector<std::string> items{ " Cancel", " Factory data reset" }; + + size_t chosen_item = ui->ShowPromptWipeDataConfirmationMenu( + headers, items, + std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2)); + + return (chosen_item == 1); } // Return true on success. @@ -394,34 +402,38 @@ 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{ + std::vector<std::string> wipe_data_menu_headers{ "Can't load Android system. Your data may be corrupt. " "If you continue to get this message, you may need to " "perform a factory data reset and erase all user data " "stored on this device.", }; // clang-format off - std::vector<std::string> items { + std::vector<std::string> wipe_data_menu_items { "Try again", "Factory data reset", }; // clang-format on for (;;) { - size_t chosen_item = ui->ShowMenu( - headers, items, 0, true, + size_t chosen_item = ui->ShowPromptWipeDataMenu( + wipe_data_menu_headers, wipe_data_menu_items, std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2)); - // If ShowMenu() returned RecoveryUI::KeyError::INTERRUPTED, WaitKey() was interrupted. if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) { - return 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; + } } } } @@ -486,62 +498,93 @@ static bool secure_wipe_partition(const std::string& partition) { return true; } -// Check if the wipe package matches expectation: +static std::unique_ptr<Package> ReadWipePackage(size_t wipe_package_size) { + if (wipe_package_size == 0) { + LOG(ERROR) << "wipe_package_size is zero"; + return nullptr; + } + + std::string wipe_package; + std::string err_str; + if (!read_wipe_package(&wipe_package, wipe_package_size, &err_str)) { + PLOG(ERROR) << "Failed to read wipe package" << err_str; + return nullptr; + } + + return Package::CreateMemoryPackage( + std::vector<uint8_t>(wipe_package.begin(), wipe_package.end()), nullptr); +} + +// Checks if the wipe package matches expectation. If the check passes, reads the list of +// partitions to wipe from the package. Checks include // 1. verify the package. // 2. check metadata (ota-type, pre-device and serial number if having one). -static bool check_wipe_package(size_t wipe_package_size) { - if (wipe_package_size == 0) { - LOG(ERROR) << "wipe_package_size is zero"; - return false; - } - std::string wipe_package; - std::string err_str; - if (!read_wipe_package(&wipe_package, wipe_package_size, &err_str)) { - PLOG(ERROR) << "Failed to read wipe package"; - return false; - } - if (!verify_package(reinterpret_cast<const unsigned char*>(wipe_package.data()), - wipe_package.size())) { - LOG(ERROR) << "Failed to verify package"; - return false; - } +static bool CheckWipePackage(Package* wipe_package) { + if (!verify_package(wipe_package)) { + LOG(ERROR) << "Failed to verify package"; + return false; + } - // Extract metadata - ZipArchiveHandle zip; - int err = OpenArchiveFromMemory(static_cast<void*>(&wipe_package[0]), wipe_package.size(), - "wipe_package", &zip); - if (err != 0) { - LOG(ERROR) << "Can't open wipe package : " << ErrorCodeString(err); - return false; + ZipArchiveHandle zip = wipe_package->GetZipArchiveHandle(); + if (!zip) { + LOG(ERROR) << "Failed to get ZipArchiveHandle"; + return false; + } + + std::map<std::string, std::string> metadata; + if (!ReadMetadataFromPackage(zip, &metadata)) { + LOG(ERROR) << "Failed to parse metadata in the zip file"; + return false; + } + + return CheckPackageMetadata(metadata, OtaType::BRICK) == 0; +} + +std::vector<std::string> GetWipePartitionList(Package* wipe_package) { + ZipArchiveHandle zip = wipe_package->GetZipArchiveHandle(); + if (!zip) { + LOG(ERROR) << "Failed to get ZipArchiveHandle"; + return {}; + } + + static constexpr const char* RECOVERY_WIPE_ENTRY_NAME = "recovery.wipe"; + + std::string partition_list_content; + ZipString path(RECOVERY_WIPE_ENTRY_NAME); + ZipEntry entry; + if (FindEntry(zip, path, &entry) == 0) { + uint32_t length = entry.uncompressed_length; + partition_list_content = std::string(length, '\0'); + if (auto err = ExtractToMemory( + zip, &entry, reinterpret_cast<uint8_t*>(partition_list_content.data()), length); + err != 0) { + LOG(ERROR) << "Failed to extract " << RECOVERY_WIPE_ENTRY_NAME << ": " + << ErrorCodeString(err); + return {}; } - std::string metadata; - if (!read_metadata_from_package(zip, &metadata)) { - CloseArchive(zip); - return false; + } else { + LOG(INFO) << "Failed to find " << RECOVERY_WIPE_ENTRY_NAME + << ", falling back to use the partition list on device."; + + static constexpr const char* RECOVERY_WIPE_ON_DEVICE = "/etc/recovery.wipe"; + if (!android::base::ReadFileToString(RECOVERY_WIPE_ON_DEVICE, &partition_list_content)) { + PLOG(ERROR) << "failed to read \"" << RECOVERY_WIPE_ON_DEVICE << "\""; + return {}; } - CloseArchive(zip); - - // Check metadata - std::vector<std::string> lines = android::base::Split(metadata, "\n"); - bool ota_type_matched = false; - bool device_type_matched = false; - bool has_serial_number = false; - bool serial_number_matched = false; - for (const auto& line : lines) { - if (line == "ota-type=BRICK") { - ota_type_matched = true; - } else if (android::base::StartsWith(line, "pre-device=")) { - std::string device_type = line.substr(strlen("pre-device=")); - std::string real_device_type = android::base::GetProperty("ro.build.product", ""); - device_type_matched = (device_type == real_device_type); - } else if (android::base::StartsWith(line, "serialno=")) { - std::string serial_no = line.substr(strlen("serialno=")); - std::string real_serial_no = android::base::GetProperty("ro.serialno", ""); - has_serial_number = true; - serial_number_matched = (serial_no == real_serial_no); - } + } + + std::vector<std::string> result; + std::vector<std::string> lines = android::base::Split(partition_list_content, "\n"); + for (const std::string& line : lines) { + std::string partition = android::base::Trim(line); + // Ignore '#' comment or empty lines. + if (android::base::StartsWith(partition, "#") || partition.empty()) { + continue; } - return ota_type_matched && device_type_matched && (!has_serial_number || serial_number_matched); + result.push_back(line); + } + + return result; } // Wipes the current A/B device, with a secure wipe of all the partitions in RECOVERY_WIPE. @@ -549,25 +592,24 @@ static bool wipe_ab_device(size_t wipe_package_size) { ui->SetBackground(RecoveryUI::ERASING); ui->SetProgressType(RecoveryUI::INDETERMINATE); - if (!check_wipe_package(wipe_package_size)) { - LOG(ERROR) << "Failed to verify wipe package"; + auto wipe_package = ReadWipePackage(wipe_package_size); + if (!wipe_package) { + LOG(ERROR) << "Failed to open wipe package"; return false; } - static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe"; - std::string partition_list; - if (!android::base::ReadFileToString(RECOVERY_WIPE, &partition_list)) { - LOG(ERROR) << "failed to read \"" << RECOVERY_WIPE << "\""; + + if (!CheckWipePackage(wipe_package.get())) { + LOG(ERROR) << "Failed to verify wipe package"; return false; } - std::vector<std::string> lines = android::base::Split(partition_list, "\n"); - for (const std::string& line : lines) { - std::string partition = android::base::Trim(line); - // Ignore '#' comment or empty lines. - if (android::base::StartsWith(partition, "#") || partition.empty()) { - continue; - } + auto partition_list = GetWipePartitionList(wipe_package.get()); + if (partition_list.empty()) { + LOG(ERROR) << "Empty wipe ab partition list"; + return false; + } + for (const auto& partition : partition_list) { // Proceed anyway even if it fails to wipe some partition. secure_wipe_partition(partition); } @@ -584,7 +626,7 @@ static void choose_recovery_file(Device* device) { log_file += "." + std::to_string(i); } - if (ensure_path_mounted(log_file.c_str()) == 0 && access(log_file.c_str(), R_OK) == 0) { + if (ensure_path_mounted(log_file) == 0 && access(log_file.c_str(), R_OK) == 0) { entries.push_back(std::move(log_file)); } }; @@ -848,14 +890,8 @@ 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 (ensure_path_mounted_at("/", "/mnt/system") != -1) { - ui->Print("Mounted /system.\n"); - } - } else { - if (ensure_path_mounted_at("/system", "/mnt/system") != -1) { - ui->Print("Mounted /system.\n"); - } + if (ensure_path_mounted_at(get_system_root(), "/mnt/system") != -1) { + ui->Print("Mounted /system.\n"); } break; @@ -1192,12 +1228,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..935d69815 100644 --- a/recovery_main.cpp +++ b/recovery_main.cpp @@ -217,9 +217,10 @@ static void ListenRecoverySocket(RecoveryUI* ui, std::atomic<Device::BuiltinActi } static void redirect_stdio(const char* filename) { - int pipefd[2]; - if (pipe(pipefd) == -1) { - PLOG(ERROR) << "pipe failed"; + android::base::unique_fd pipe_read, pipe_write; + // Create a pipe that allows parent process sending logs over. + if (!android::base::Pipe(&pipe_read, &pipe_write)) { + PLOG(ERROR) << "Failed to create pipe for redirecting stdio"; // Fall back to traditional logging mode without timestamps. If these fail, there's not really // anywhere to complain... @@ -233,7 +234,7 @@ static void redirect_stdio(const char* filename) { pid_t pid = fork(); if (pid == -1) { - PLOG(ERROR) << "fork failed"; + PLOG(ERROR) << "Failed to fork for redirecting stdio"; // Fall back to traditional logging mode without timestamps. If these fail, there's not really // anywhere to complain... @@ -246,8 +247,8 @@ static void redirect_stdio(const char* filename) { } if (pid == 0) { - /// Close the unused write end. - close(pipefd[1]); + // Child process reads the incoming logs and doesn't write to the pipe. + pipe_write.reset(); auto start = std::chrono::steady_clock::now(); @@ -255,15 +256,13 @@ static void redirect_stdio(const char* filename) { FILE* log_fp = fopen(filename, "ae"); if (log_fp == nullptr) { PLOG(ERROR) << "fopen \"" << filename << "\" failed"; - close(pipefd[0]); _exit(EXIT_FAILURE); } - FILE* pipe_fp = fdopen(pipefd[0], "r"); + FILE* pipe_fp = android::base::Fdopen(std::move(pipe_read), "r"); if (pipe_fp == nullptr) { PLOG(ERROR) << "fdopen failed"; check_and_fclose(log_fp, filename); - close(pipefd[0]); _exit(EXIT_FAILURE); } @@ -283,25 +282,23 @@ static void redirect_stdio(const char* filename) { PLOG(ERROR) << "getline failed"; + fclose(pipe_fp); free(line); check_and_fclose(log_fp, filename); - close(pipefd[0]); _exit(EXIT_FAILURE); } else { // Redirect stdout/stderr to the logger process. Close the unused read end. - close(pipefd[0]); + pipe_read.reset(); setbuf(stdout, nullptr); setbuf(stderr, nullptr); - if (dup2(pipefd[1], STDOUT_FILENO) == -1) { + if (dup2(pipe_write.get(), STDOUT_FILENO) == -1) { PLOG(ERROR) << "dup2 stdout failed"; } - if (dup2(pipefd[1], STDERR_FILENO) == -1) { + if (dup2(pipe_write.get(), STDERR_FILENO) == -1) { PLOG(ERROR) << "dup2 stderr failed"; } - - close(pipefd[1]); } } @@ -364,7 +361,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.dynamic_partitions", false)) { fastboot = true; } break; @@ -425,6 +423,10 @@ int main(int argc, char** argv) { device->RemoveMenuItemForAction(Device::WIPE_CACHE); } + if (!android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) { + device->RemoveMenuItemForAction(Device::ENTER_FASTBOOT); + } + ui->SetBackground(RecoveryUI::NONE); if (show_text) ui->ShowText(true); @@ -453,6 +455,8 @@ int main(int argc, char** argv) { } } + ui->SetEnableFastbootdLogo(fastboot); + auto ret = fastboot ? StartFastboot(device, args) : start_recovery(device, args); if (ret == Device::KEY_INTERRUPTED) { @@ -473,8 +477,13 @@ int main(int argc, char** argv) { break; case Device::ENTER_FASTBOOT: - LOG(INFO) << "Entering fastboot"; - fastboot = true; + if (logical_partitions_mapped()) { + ui->Print("Partitions may be mounted - rebooting to enter fastboot."); + android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,fastboot"); + } else { + LOG(INFO) << "Entering fastboot"; + fastboot = true; + } break; case Device::ENTER_RECOVERY: diff --git a/res-hdpi/images/fastbootd.png b/res-hdpi/images/fastbootd.png Binary files differnew file mode 100644 index 000000000..5127b3b37 --- /dev/null +++ b/res-hdpi/images/fastbootd.png diff --git a/res-mdpi/images/fastbootd.png b/res-mdpi/images/fastbootd.png Binary files differnew file mode 100644 index 000000000..c1b7bc2fb --- /dev/null +++ b/res-mdpi/images/fastbootd.png diff --git a/res-xhdpi/images/fastbootd.png b/res-xhdpi/images/fastbootd.png Binary files differnew file mode 100644 index 000000000..e3da85ad7 --- /dev/null +++ b/res-xhdpi/images/fastbootd.png diff --git a/res-xxhdpi/images/fastbootd.png b/res-xxhdpi/images/fastbootd.png Binary files differnew file mode 100644 index 000000000..482dbb29c --- /dev/null +++ b/res-xxhdpi/images/fastbootd.png diff --git a/res-xxxhdpi/images/fastbootd.png b/res-xxxhdpi/images/fastbootd.png Binary files differnew file mode 100644 index 000000000..a878b1f06 --- /dev/null +++ b/res-xxxhdpi/images/fastbootd.png @@ -18,6 +18,7 @@ #include <ctype.h> #include <fcntl.h> +#include <inttypes.h> #include <stdint.h> #include <stdlib.h> #include <string.h> @@ -27,7 +28,7 @@ #include <sys/wait.h> #include <unistd.h> -#include <algorithm> +#include <iostream> #include <string> #include <vector> @@ -38,155 +39,60 @@ #include <cryptfs.h> #include <ext4_utils/wipe.h> #include <fs_mgr.h> +#include <fs_mgr/roots.h> +#include <fs_mgr_dm_linear.h> #include "otautil/mounts.h" +#include "otautil/sysutil.h" -static struct fstab* fstab = nullptr; +using android::fs_mgr::Fstab; +using android::fs_mgr::FstabEntry; +using android::fs_mgr::ReadDefaultFstab; + +static Fstab fstab; extern struct selabel_handle* sehandle; void load_volume_table() { - fstab = fs_mgr_read_fstab_default(); - if (!fstab) { + if (!ReadDefaultFstab(&fstab)) { LOG(ERROR) << "Failed to read default fstab"; return; } - int ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk"); - if (ret == -1) { - LOG(ERROR) << "Failed to add /tmp entry to fstab"; - fs_mgr_free_fstab(fstab); - fstab = nullptr; - return; - } + fstab.emplace_back(FstabEntry{ + .mount_point = "/tmp", .fs_type = "ramdisk", .blk_device = "ramdisk", .length = 0 }); - printf("recovery filesystem table\n"); - printf("=========================\n"); - for (int i = 0; i < fstab->num_entries; ++i) { - const Volume* v = &fstab->recs[i]; - printf(" %d %s %s %s %lld\n", i, v->mount_point, v->fs_type, v->blk_device, v->length); + std::cout << "recovery filesystem table" << std::endl << "=========================" << std::endl; + for (size_t i = 0; i < fstab.size(); ++i) { + const auto& entry = fstab[i]; + std::cout << " " << i << " " << entry.mount_point << " " + << " " << entry.fs_type << " " << entry.blk_device << " " << entry.length + << std::endl; } - printf("\n"); + std::cout << std::endl; } Volume* volume_for_mount_point(const std::string& mount_point) { - return fs_mgr_get_entry_for_mount_point(fstab, mount_point); -} - -// Finds the volume specified by the given path. fs_mgr_get_entry_for_mount_point() does exact match -// only, so it attempts the prefixes recursively (e.g. "/cache/recovery/last_log", -// "/cache/recovery", "/cache", "/" for a given path of "/cache/recovery/last_log") and returns the -// first match or nullptr. -static Volume* volume_for_path(const char* path) { - if (path == nullptr || path[0] == '\0') return nullptr; - std::string str(path); - while (true) { - Volume* result = fs_mgr_get_entry_for_mount_point(fstab, str); - if (result != nullptr || str == "/") { - return result; - } - size_t slash = str.find_last_of('/'); - if (slash == std::string::npos) return nullptr; - if (slash == 0) { - str = "/"; - } else { - str = str.substr(0, slash); - } - } - return nullptr; + return android::fs_mgr::GetEntryForMountPoint(&fstab, mount_point); } // Mount the volume specified by path at the given mount_point. -int ensure_path_mounted_at(const char* path, const char* mount_point) { - Volume* v = volume_for_path(path); - if (v == nullptr) { - LOG(ERROR) << "unknown volume for path [" << path << "]"; - return -1; - } - if (strcmp(v->fs_type, "ramdisk") == 0) { - // The ramdisk is always mounted. - return 0; - } - - if (!scan_mounted_volumes()) { - LOG(ERROR) << "Failed to scan mounted volumes"; - return -1; - } - - if (!mount_point) { - mount_point = v->mount_point; - } - - const MountedVolume* mv = find_mounted_volume_by_mount_point(mount_point); - if (mv != nullptr) { - // Volume is already mounted. - return 0; - } - - mkdir(mount_point, 0755); // in case it doesn't already exist - - if (strcmp(v->fs_type, "ext4") == 0 || strcmp(v->fs_type, "squashfs") == 0 || - strcmp(v->fs_type, "vfat") == 0 || strcmp(v->fs_type, "f2fs") == 0) { - int result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options); - if (result == -1 && fs_mgr_is_formattable(v)) { - PLOG(ERROR) << "Failed to mount " << mount_point << "; formatting"; - bool crypt_footer = fs_mgr_is_encryptable(v) && !strcmp(v->key_loc, "footer"); - if (fs_mgr_do_format(v, crypt_footer) == 0) { - result = mount(v->blk_device, mount_point, v->fs_type, v->flags, v->fs_options); - } else { - PLOG(ERROR) << "Failed to format " << mount_point; - return -1; - } - } - - if (result == -1) { - PLOG(ERROR) << "Failed to mount " << mount_point; - return -1; - } - return 0; - } - - LOG(ERROR) << "unknown fs_type \"" << v->fs_type << "\" for " << mount_point; - return -1; +int ensure_path_mounted_at(const std::string& path, const std::string& mount_point) { + return android::fs_mgr::EnsurePathMounted(&fstab, path, mount_point) ? 0 : -1; } -int ensure_path_mounted(const char* path) { +int ensure_path_mounted(const std::string& path) { // Mount at the default mount point. - return ensure_path_mounted_at(path, nullptr); + return android::fs_mgr::EnsurePathMounted(&fstab, path) ? 0 : -1; } -int ensure_path_unmounted(const char* path) { - const Volume* v = volume_for_path(path); - if (v == nullptr) { - LOG(ERROR) << "unknown volume for path [" << path << "]"; - return -1; - } - if (strcmp(v->fs_type, "ramdisk") == 0) { - // The ramdisk is always mounted; you can't unmount it. - return -1; - } - - if (!scan_mounted_volumes()) { - LOG(ERROR) << "Failed to scan mounted volumes"; - return -1; - } - - MountedVolume* mv = find_mounted_volume_by_mount_point(v->mount_point); - if (mv == nullptr) { - // Volume is already unmounted. - return 0; - } - - return unmount_mounted_volume(mv); +int ensure_path_unmounted(const std::string& path) { + return android::fs_mgr::EnsurePathUnmounted(&fstab, path) ? 0 : -1; } static int exec_cmd(const std::vector<std::string>& args) { - CHECK_NE(static_cast<size_t>(0), args.size()); - - std::vector<char*> argv(args.size()); - std::transform(args.cbegin(), args.cend(), argv.begin(), - [](const std::string& arg) { return const_cast<char*>(arg.c_str()); }); - argv.push_back(nullptr); + CHECK(!args.empty()); + auto argv = StringVectorToNullTerminatedArray(args); pid_t child; if ((child = fork()) == 0) { @@ -225,17 +131,17 @@ static int64_t get_file_size(int fd, uint64_t reserve_len) { return computed_size; } -int format_volume(const char* volume, const char* directory) { - const Volume* v = volume_for_path(volume); +int format_volume(const std::string& volume, const std::string& directory) { + const FstabEntry* v = android::fs_mgr::GetEntryForPath(&fstab, volume); if (v == nullptr) { LOG(ERROR) << "unknown volume \"" << volume << "\""; return -1; } - if (strcmp(v->fs_type, "ramdisk") == 0) { + if (v->fs_type == "ramdisk") { LOG(ERROR) << "can't format_volume \"" << volume << "\""; return -1; } - if (strcmp(v->mount_point, volume) != 0) { + if (v->mount_point != volume) { LOG(ERROR) << "can't give path \"" << volume << "\" to format_volume"; return -1; } @@ -243,16 +149,16 @@ int format_volume(const char* volume, const char* directory) { LOG(ERROR) << "format_volume: Failed to unmount \"" << v->mount_point << "\""; return -1; } - if (strcmp(v->fs_type, "ext4") != 0 && strcmp(v->fs_type, "f2fs") != 0) { + if (v->fs_type != "ext4" && v->fs_type != "f2fs") { LOG(ERROR) << "format_volume: fs_type \"" << v->fs_type << "\" unsupported"; return -1; } // If there's a key_loc that looks like a path, it should be a block device for storing encryption // metadata. Wipe it too. - if (v->key_loc != nullptr && v->key_loc[0] == '/') { + if (!v->key_loc.empty() && v->key_loc[0] == '/') { LOG(INFO) << "Wiping " << v->key_loc; - int fd = open(v->key_loc, O_WRONLY | O_CREAT, 0644); + int fd = open(v->key_loc.c_str(), O_WRONLY | O_CREAT, 0644); if (fd == -1) { PLOG(ERROR) << "format_volume: Failed to open " << v->key_loc; return -1; @@ -264,9 +170,8 @@ int format_volume(const char* volume, const char* directory) { int64_t length = 0; if (v->length > 0) { length = v->length; - } else if (v->length < 0 || - (v->key_loc != nullptr && strcmp(v->key_loc, "footer") == 0)) { - android::base::unique_fd fd(open(v->blk_device, O_RDONLY)); + } else if (v->length < 0 || v->key_loc == "footer") { + android::base::unique_fd fd(open(v->blk_device.c_str(), O_RDONLY)); if (fd == -1) { PLOG(ERROR) << "format_volume: failed to open " << v->blk_device; return -1; @@ -280,7 +185,7 @@ int format_volume(const char* volume, const char* directory) { } } - if (strcmp(v->fs_type, "ext4") == 0) { + if (v->fs_type == "ext4") { static constexpr int kBlockSize = 4096; std::vector<std::string> mke2fs_args = { "/system/bin/mke2fs", "-F", "-t", "ext4", "-b", std::to_string(kBlockSize), @@ -303,7 +208,7 @@ int format_volume(const char* volume, const char* directory) { } int result = exec_cmd(mke2fs_args); - if (result == 0 && directory != nullptr) { + if (result == 0 && !directory.empty()) { std::vector<std::string> e2fsdroid_args = { "/system/bin/e2fsdroid", "-e", "-f", directory, "-a", volume, v->blk_device, }; @@ -319,71 +224,66 @@ int format_volume(const char* volume, const char* directory) { // Has to be f2fs because we checked earlier. static constexpr int kSectorSize = 4096; - std::string cmd("/sbin/mkfs.f2fs"); - // clang-format off std::vector<std::string> make_f2fs_cmd = { - cmd, - "-d1", - "-f", - "-O", "encrypt", - "-O", "quota", - "-O", "verity", - "-w", std::to_string(kSectorSize), + "/system/bin/make_f2fs", + "-g", + "android", v->blk_device, }; - // clang-format on if (length >= kSectorSize) { make_f2fs_cmd.push_back(std::to_string(length / kSectorSize)); } - int result = exec_cmd(make_f2fs_cmd); - if (result == 0 && directory != nullptr) { - cmd = "/sbin/sload.f2fs"; - // clang-format off + if (exec_cmd(make_f2fs_cmd) != 0) { + PLOG(ERROR) << "format_volume: Failed to make_f2fs on " << v->blk_device; + return -1; + } + if (!directory.empty()) { std::vector<std::string> sload_f2fs_cmd = { - cmd, - "-f", directory, - "-t", volume, - v->blk_device, + "/system/bin/sload_f2fs", "-f", directory, "-t", volume, v->blk_device, }; - // clang-format on - result = exec_cmd(sload_f2fs_cmd); - } - if (result != 0) { - PLOG(ERROR) << "format_volume: Failed " << cmd << " on " << v->blk_device; - return -1; + if (exec_cmd(sload_f2fs_cmd) != 0) { + PLOG(ERROR) << "format_volume: Failed to sload_f2fs on " << v->blk_device; + return -1; + } } return 0; } -int format_volume(const char* volume) { - return format_volume(volume, nullptr); +int format_volume(const std::string& volume) { + return format_volume(volume, ""); } int setup_install_mounts() { - if (fstab == nullptr) { + if (fstab.empty()) { LOG(ERROR) << "can't set up install mounts: no fstab loaded"; return -1; } - for (int i = 0; i < fstab->num_entries; ++i) { - const Volume* v = fstab->recs + i; - + for (const FstabEntry& entry : fstab) { // We don't want to do anything with "/". - if (strcmp(v->mount_point, "/") == 0) { + if (entry.mount_point == "/") { continue; } - if (strcmp(v->mount_point, "/tmp") == 0 || strcmp(v->mount_point, "/cache") == 0) { - if (ensure_path_mounted(v->mount_point) != 0) { - LOG(ERROR) << "Failed to mount " << v->mount_point; + if (entry.mount_point == "/tmp" || entry.mount_point == "/cache") { + if (ensure_path_mounted(entry.mount_point) != 0) { + LOG(ERROR) << "Failed to mount " << entry.mount_point; return -1; } } else { - if (ensure_path_unmounted(v->mount_point) != 0) { - LOG(ERROR) << "Failed to unmount " << v->mount_point; + if (ensure_path_unmounted(entry.mount_point) != 0) { + LOG(ERROR) << "Failed to unmount " << entry.mount_point; return -1; } } } return 0; } + +bool logical_partitions_mapped() { + return android::fs_mgr::LogicalPartitionsMapped(); +} + +std::string get_system_root() { + return android::fs_mgr::GetSystemRoot(); +} @@ -19,7 +19,9 @@ #include <string> -typedef struct fstab_rec Volume; +#include <fstab/fstab.h> + +using Volume = android::fs_mgr::FstabEntry; // Load and parse volume data from /etc/recovery.fstab. void load_volume_table(); @@ -29,28 +31,32 @@ Volume* volume_for_mount_point(const std::string& mount_point); // Make sure that the volume 'path' is on is mounted. Returns 0 on // success (volume is mounted). -int ensure_path_mounted(const char* path); +int ensure_path_mounted(const std::string& path); // Similar to ensure_path_mounted, but allows one to specify the mount_point. -int ensure_path_mounted_at(const char* path, const char* mount_point); +int ensure_path_mounted_at(const std::string& path, const std::string& mount_point); // Make sure that the volume 'path' is on is unmounted. Returns 0 on // success (volume is unmounted); -int ensure_path_unmounted(const char* path); +int ensure_path_unmounted(const std::string& path); // Reformat the given volume (must be the mount point only, eg // "/cache"), no paths permitted. Attempts to unmount the volume if // it is mounted. -int format_volume(const char* volume); +int format_volume(const std::string& volume); // Reformat the given volume (must be the mount point only, eg // "/cache"), no paths permitted. Attempts to unmount the volume if // it is mounted. // Copies 'directory' to root of the newly formatted volume -int format_volume(const char* volume, const char* directory); +int format_volume(const std::string& volume, const std::string& directory); // Ensure that all and only the volumes that packages expect to find // mounted (/tmp and /cache) are mounted. Returns 0 on success. int setup_install_mounts(); +bool logical_partitions_mapped(); + +std::string get_system_root(); + #endif // RECOVERY_ROOTS_H_ diff --git a/screen_ui.cpp b/screen_ui.cpp index 391dedb00..6f2b68b41 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,157 @@ 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(const GRSurface* graphic_headers, + const std::vector<const GRSurface*>& graphic_items, + size_t initial_selection, const DrawInterface& draw_funcs) + : Menu(initial_selection, draw_funcs) { + graphic_headers_ = graphic_headers->Clone(); + graphic_items_.reserve(graphic_items.size()); + for (const auto& item : graphic_items) { + graphic_items_.emplace_back(item->Clone()); + } +} + +int GraphicMenu::Select(int sel) { + CHECK_LE(graphic_items_.size(), static_cast<size_t>(std::numeric_limits<int>::max())); + int count = graphic_items_.size(); + + // Wraps the selection at boundary if the menu is not scrollable. + if (sel < 0) { + selection_ = count - 1; + } else if (sel >= count) { + selection_ = 0; + } else { + selection_ = sel; + } + + return selection_; +} + +int GraphicMenu::DrawHeader(int x, int y) const { + draw_funcs_.SetColor(UIElement::HEADER); + draw_funcs_.DrawTextIcon(x, y, graphic_headers_.get()); + return graphic_headers_->height; +} + +int GraphicMenu::DrawItems(int x, int y, int screen_width, bool long_press) const { + int offset = 0; + + draw_funcs_.SetColor(UIElement::MENU); + offset += draw_funcs_.DrawHorizontalRule(y + offset) + 4; + + for (size_t i = 0; i < graphic_items_.size(); i++) { + auto& item = graphic_items_[i]; + if (i == selection_) { + draw_funcs_.SetColor(long_press ? UIElement::MENU_SEL_BG_ACTIVE : UIElement::MENU_SEL_BG); + + int bar_height = item->height + 4; + draw_funcs_.DrawHighlightBar(0, y + offset - 2, screen_width, bar_height); + + // Bold white text for the selected item. + draw_funcs_.SetColor(UIElement::MENU_SEL_FG); + } + draw_funcs_.DrawTextIcon(x, y + offset, item.get()); + offset += item->height; + + draw_funcs_.SetColor(UIElement::MENU); + } + offset += draw_funcs_.DrawHorizontalRule(y + offset); + + return offset; +} + +bool GraphicMenu::Validate(size_t max_width, size_t max_height, const GRSurface* graphic_headers, + const std::vector<const GRSurface*>& graphic_items) { + int offset = 0; + if (!ValidateGraphicSurface(max_width, max_height, offset, graphic_headers)) { + return false; + } + offset += graphic_headers->height; + + for (const auto& item : graphic_items) { + if (!ValidateGraphicSurface(max_width, max_height, offset, item)) { + return false; + } + offset += item->height; + } + + return true; +} + +bool GraphicMenu::ValidateGraphicSurface(size_t max_width, size_t max_height, int y, + const GRSurface* surface) { + if (!surface) { + fprintf(stderr, "Graphic surface can not be null\n"); + return false; + } + + if (surface->pixel_bytes != 1 || surface->width != surface->row_bytes) { + fprintf(stderr, "Invalid graphic surface, pixel bytes: %zu, width: %zu row_bytes: %zu\n", + surface->pixel_bytes, surface->width, surface->row_bytes); + return false; + } + + if (surface->width > max_width || surface->height > max_height - y) { + fprintf(stderr, + "Graphic surface doesn't fit into the screen. width: %zu, height: %zu, max_width: %zu," + " max_height: %zu, vertical offset: %d\n", + surface->width, surface->height, max_width, max_height, y); + return false; + } + + return true; +} + ScreenRecoveryUI::ScreenRecoveryUI() : ScreenRecoveryUI(false) {} constexpr int kDefaultMarginHeight = 0; @@ -154,7 +313,9 @@ ScreenRecoveryUI::ScreenRecoveryUI(bool scrollable_menu) animation_fps_( android::base::GetIntProperty("ro.recovery.ui.animation_fps", kDefaultAnimationFps)), density_(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f), - currentIcon(NONE), + current_icon_(NONE), + current_frame_(0), + intro_done_(false), progressBarType(EMPTY), progressScopeStart(0), progressScopeSize(0), @@ -169,10 +330,6 @@ ScreenRecoveryUI::ScreenRecoveryUI(bool scrollable_menu) show_text_ever(false), scrollable_menu_(scrollable_menu), file_viewer_text_(nullptr), - intro_frames(0), - loop_frames(0), - current_frame(0), - intro_done(false), stage(-1), max_stage(-1), locale_(""), @@ -187,23 +344,23 @@ ScreenRecoveryUI::~ScreenRecoveryUI() { gr_exit(); } -GRSurface* ScreenRecoveryUI::GetCurrentFrame() const { - if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) { - return intro_done ? loopFrames[current_frame] : introFrames[current_frame]; +const GRSurface* ScreenRecoveryUI::GetCurrentFrame() const { + if (current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) { + return intro_done_ ? loop_frames_[current_frame_].get() : intro_frames_[current_frame_].get(); } - return error_icon; + return error_icon_.get(); } -GRSurface* ScreenRecoveryUI::GetCurrentText() const { - switch (currentIcon) { +const GRSurface* ScreenRecoveryUI::GetCurrentText() const { + switch (current_icon_) { case ERASING: - return erasing_text; + return erasing_text_.get(); case ERROR: - return error_text; + return error_text_.get(); case INSTALLING_UPDATE: - return installing_text; + return installing_text_.get(); case NO_COMMAND: - return no_command_text; + return no_command_text_.get(); case NONE: abort(); } @@ -238,20 +395,21 @@ static constexpr int kLayouts[LAYOUT_MAX][DIMENSION_MAX] = { }; int ScreenRecoveryUI::GetAnimationBaseline() const { - return GetTextBaseline() - PixelsFromDp(kLayouts[layout_][ICON]) - gr_get_height(loopFrames[0]); + return GetTextBaseline() - PixelsFromDp(kLayouts[layout_][ICON]) - + gr_get_height(loop_frames_[0].get()); } int ScreenRecoveryUI::GetTextBaseline() const { return GetProgressBaseline() - PixelsFromDp(kLayouts[layout_][TEXT]) - - gr_get_height(installing_text); + gr_get_height(installing_text_.get()); } int ScreenRecoveryUI::GetProgressBaseline() const { - int elements_sum = gr_get_height(loopFrames[0]) + PixelsFromDp(kLayouts[layout_][ICON]) + - gr_get_height(installing_text) + PixelsFromDp(kLayouts[layout_][TEXT]) + - gr_get_height(progressBarFill); + int elements_sum = gr_get_height(loop_frames_[0].get()) + PixelsFromDp(kLayouts[layout_][ICON]) + + gr_get_height(installing_text_.get()) + PixelsFromDp(kLayouts[layout_][TEXT]) + + gr_get_height(progress_bar_fill_.get()); int bottom_gap = (ScreenHeight() - elements_sum) / 2; - return ScreenHeight() - bottom_gap - gr_get_height(progressBarFill); + return ScreenHeight() - bottom_gap - gr_get_height(progress_bar_fill_.get()); } // Clear the screen and draw the currently selected background icon (if any). @@ -260,20 +418,20 @@ void ScreenRecoveryUI::draw_background_locked() { pagesIdentical = false; gr_color(0, 0, 0, 255); gr_clear(); - if (currentIcon != NONE) { + if (current_icon_ != NONE) { if (max_stage != -1) { - int stage_height = gr_get_height(stageMarkerEmpty); - int stage_width = gr_get_width(stageMarkerEmpty); - int x = (ScreenWidth() - max_stage * gr_get_width(stageMarkerEmpty)) / 2; + int stage_height = gr_get_height(stage_marker_empty_.get()); + int stage_width = gr_get_width(stage_marker_empty_.get()); + int x = (ScreenWidth() - max_stage * gr_get_width(stage_marker_empty_.get())) / 2; int y = ScreenHeight() - stage_height - margin_height_; for (int i = 0; i < max_stage; ++i) { - GRSurface* stage_surface = (i < stage) ? stageMarkerFill : stageMarkerEmpty; - DrawSurface(stage_surface, 0, 0, stage_width, stage_height, x, y); + const auto& stage_surface = (i < stage) ? stage_marker_fill_ : stage_marker_empty_; + DrawSurface(stage_surface.get(), 0, 0, stage_width, stage_height, x, y); x += stage_width; } } - GRSurface* text_surface = GetCurrentText(); + const auto& text_surface = GetCurrentText(); int text_x = (ScreenWidth() - gr_get_width(text_surface)) / 2; int text_y = GetTextBaseline(); gr_color(255, 255, 255, 255); @@ -284,8 +442,8 @@ void ScreenRecoveryUI::draw_background_locked() { // Draws the animation and progress bar (if any) on the screen. Does not flip pages. Should only be // called with updateMutex locked. void ScreenRecoveryUI::draw_foreground_locked() { - if (currentIcon != NONE) { - GRSurface* frame = GetCurrentFrame(); + if (current_icon_ != NONE) { + const auto& frame = GetCurrentFrame(); int frame_width = gr_get_width(frame); int frame_height = gr_get_height(frame); int frame_x = (ScreenWidth() - frame_width) / 2; @@ -294,8 +452,8 @@ void ScreenRecoveryUI::draw_foreground_locked() { } if (progressBarType != EMPTY) { - int width = gr_get_width(progressBarEmpty); - int height = gr_get_height(progressBarEmpty); + int width = gr_get_width(progress_bar_empty_.get()); + int height = gr_get_height(progress_bar_empty_.get()); int progress_x = (ScreenWidth() - width) / 2; int progress_y = GetProgressBaseline(); @@ -311,19 +469,20 @@ void ScreenRecoveryUI::draw_foreground_locked() { if (rtl_locale_) { // Fill the progress bar from right to left. if (pos > 0) { - DrawSurface(progressBarFill, width - pos, 0, pos, height, progress_x + width - pos, - progress_y); + DrawSurface(progress_bar_fill_.get(), width - pos, 0, pos, height, + progress_x + width - pos, progress_y); } if (pos < width - 1) { - DrawSurface(progressBarEmpty, 0, 0, width - pos, height, progress_x, progress_y); + DrawSurface(progress_bar_empty_.get(), 0, 0, width - pos, height, progress_x, progress_y); } } else { // Fill the progress bar from left to right. if (pos > 0) { - DrawSurface(progressBarFill, 0, 0, pos, height, progress_x, progress_y); + DrawSurface(progress_bar_fill_.get(), 0, 0, pos, height, progress_x, progress_y); } if (pos < width - 1) { - DrawSurface(progressBarEmpty, pos, 0, width - pos, height, progress_x + pos, progress_y); + DrawSurface(progress_bar_empty_.get(), pos, 0, width - pos, height, progress_x + pos, + progress_y); } } } @@ -332,26 +491,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: @@ -365,15 +524,14 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string SetLocale(locales_entries[sel]); std::vector<std::string> text_name = { "erasing_text", "error_text", "installing_text", "installing_security_text", "no_command_text" }; - std::unordered_map<std::string, std::unique_ptr<GRSurface, decltype(&free)>> surfaces; + std::unordered_map<std::string, std::unique_ptr<GRSurface>> surfaces; for (const auto& name : text_name) { - GRSurface* text_image = nullptr; - LoadLocalizedBitmap(name.c_str(), &text_image); + auto text_image = LoadLocalizedBitmap(name); if (!text_image) { Print("Failed to load %s\n", name.c_str()); return; } - surfaces.emplace(name, std::unique_ptr<GRSurface, decltype(&free)>(text_image, &free)); + surfaces.emplace(name, std::move(text_image)); } std::lock_guard<std::mutex> lg(updateMutex); @@ -384,7 +542,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 +558,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()); @@ -447,7 +605,7 @@ int ScreenRecoveryUI::ScreenHeight() const { return gr_fb_height(); } -void ScreenRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, +void ScreenRecoveryUI::DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const { gr_blit(surface, sx, sy, w, h, dx, dy); } @@ -465,7 +623,7 @@ void ScreenRecoveryUI::DrawFill(int x, int y, int w, int h) const { gr_fill(x, y, w, h); } -void ScreenRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const { +void ScreenRecoveryUI::DrawTextIcon(int x, int y, const GRSurface* surface) const { gr_texticon(x, y, surface); } @@ -543,11 +701,20 @@ void ScreenRecoveryUI::draw_screen_locked() { void ScreenRecoveryUI::draw_menu_and_text_buffer_locked( const std::vector<std::string>& help_message) { int y = margin_height_; + + if (fastbootd_logo_ && fastbootd_logo_enabled_) { + // Try to get this centered on screen. + auto width = gr_get_width(fastbootd_logo_.get()); + auto height = gr_get_height(fastbootd_logo_.get()); + auto centered_x = ScreenWidth() / 2 - width / 2; + DrawSurface(fastbootd_logo_.get(), 0, 0, width, height, centered_x, y); + y += height; + } + if (menu_) { - 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 +722,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_; @@ -638,16 +768,16 @@ void ScreenRecoveryUI::ProgressThreadLoop() { // update the installation animation, if active // skip this if we have a text overlay (too expensive to update) - if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) && !show_text) { - if (!intro_done) { - if (current_frame == intro_frames - 1) { - intro_done = true; - current_frame = 0; + if ((current_icon_ == INSTALLING_UPDATE || current_icon_ == ERASING) && !show_text) { + if (!intro_done_) { + if (current_frame_ == intro_frames_.size() - 1) { + intro_done_ = true; + current_frame_ = 0; } else { - ++current_frame; + ++current_frame_; } } else { - current_frame = (current_frame + 1) % loop_frames; + current_frame_ = (current_frame_ + 1) % loop_frames_.size(); } redraw = true; @@ -676,18 +806,23 @@ void ScreenRecoveryUI::ProgressThreadLoop() { } } -void ScreenRecoveryUI::LoadBitmap(const char* filename, GRSurface** surface) { - int result = res_create_display_surface(filename, surface); - if (result < 0) { - LOG(ERROR) << "couldn't load bitmap " << filename << " (error " << result << ")"; +std::unique_ptr<GRSurface> ScreenRecoveryUI::LoadBitmap(const std::string& filename) { + GRSurface* surface; + if (auto result = res_create_display_surface(filename.c_str(), &surface); result < 0) { + LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")"; + return nullptr; } + return std::unique_ptr<GRSurface>(surface); } -void ScreenRecoveryUI::LoadLocalizedBitmap(const char* filename, GRSurface** surface) { - int result = res_create_localized_alpha_surface(filename, locale_.c_str(), surface); - if (result < 0) { - LOG(ERROR) << "couldn't load bitmap " << filename << " (error " << result << ")"; +std::unique_ptr<GRSurface> ScreenRecoveryUI::LoadLocalizedBitmap(const std::string& filename) { + GRSurface* surface; + if (auto result = res_create_localized_alpha_surface(filename.c_str(), locale_.c_str(), &surface); + result < 0) { + LOG(ERROR) << "Failed to load bitmap " << filename << " (error " << result << ")"; + return nullptr; } + return std::unique_ptr<GRSurface>(surface); } static char** Alloc2d(size_t rows, size_t cols) { @@ -702,9 +837,9 @@ static char** Alloc2d(size_t rows, size_t cols) { // Choose the right background string to display during update. void ScreenRecoveryUI::SetSystemUpdateText(bool security_update) { if (security_update) { - LoadLocalizedBitmap("installing_security_text", &installing_text); + installing_text_ = LoadLocalizedBitmap("installing_security_text"); } else { - LoadLocalizedBitmap("installing_text", &installing_text); + installing_text_ = LoadLocalizedBitmap("installing_text"); } Redraw(); } @@ -720,6 +855,16 @@ bool ScreenRecoveryUI::InitTextParams() { return true; } +bool ScreenRecoveryUI::LoadWipeDataMenuText() { + // Ignores the errors since the member variables will stay as nullptr. + cancel_wipe_data_text_ = LoadLocalizedBitmap("cancel_wipe_data_text"); + factory_data_reset_text_ = LoadLocalizedBitmap("factory_data_reset_text"); + try_again_text_ = LoadLocalizedBitmap("try_again_text"); + wipe_data_confirmation_text_ = LoadLocalizedBitmap("wipe_data_confirmation_text"); + wipe_data_menu_header_text_ = LoadLocalizedBitmap("wipe_data_menu_header_text"); + return true; +} + bool ScreenRecoveryUI::Init(const std::string& locale) { RecoveryUI::Init(locale); @@ -744,21 +889,26 @@ bool ScreenRecoveryUI::Init(const std::string& locale) { // Set up the locale info. SetLocale(locale); - LoadBitmap("icon_error", &error_icon); + error_icon_ = LoadBitmap("icon_error"); + + progress_bar_empty_ = LoadBitmap("progress_empty"); + progress_bar_fill_ = LoadBitmap("progress_fill"); + stage_marker_empty_ = LoadBitmap("stage_empty"); + stage_marker_fill_ = LoadBitmap("stage_fill"); - LoadBitmap("progress_empty", &progressBarEmpty); - LoadBitmap("progress_fill", &progressBarFill); + erasing_text_ = LoadLocalizedBitmap("erasing_text"); + no_command_text_ = LoadLocalizedBitmap("no_command_text"); + error_text_ = LoadLocalizedBitmap("error_text"); + + if (android::base::GetBoolProperty("ro.boot.dynamic_partitions", false)) { + fastbootd_logo_ = LoadBitmap("fastbootd"); + } - LoadBitmap("stage_empty", &stageMarkerEmpty); - LoadBitmap("stage_fill", &stageMarkerFill); + // Background text for "installing_update" could be "installing update" or + // "installing security update". It will be set after Init() according to the commands in BCB. + installing_text_.reset(); - // Background text for "installing_update" could be "installing update" - // or "installing security update". It will be set after UI init according - // to commands in BCB. - installing_text = nullptr; - LoadLocalizedBitmap("erasing_text", &erasing_text); - LoadLocalizedBitmap("no_command_text", &no_command_text); - LoadLocalizedBitmap("error_text", &error_text); + LoadWipeDataMenuText(); LoadAnimation(); @@ -788,32 +938,34 @@ void ScreenRecoveryUI::LoadAnimation() { } } - intro_frames = intro_frame_names.size(); - loop_frames = loop_frame_names.size(); + size_t intro_frames = intro_frame_names.size(); + size_t loop_frames = loop_frame_names.size(); // It's okay to not have an intro. - if (intro_frames == 0) intro_done = true; + if (intro_frames == 0) intro_done_ = true; // But you must have an animation. if (loop_frames == 0) abort(); std::sort(intro_frame_names.begin(), intro_frame_names.end()); std::sort(loop_frame_names.begin(), loop_frame_names.end()); - introFrames = new GRSurface*[intro_frames]; - for (size_t i = 0; i < intro_frames; i++) { - LoadBitmap(intro_frame_names.at(i).c_str(), &introFrames[i]); + intro_frames_.clear(); + intro_frames_.reserve(intro_frames); + for (const auto& frame_name : intro_frame_names) { + intro_frames_.emplace_back(LoadBitmap(frame_name)); } - loopFrames = new GRSurface*[loop_frames]; - for (size_t i = 0; i < loop_frames; i++) { - LoadBitmap(loop_frame_names.at(i).c_str(), &loopFrames[i]); + loop_frames_.clear(); + loop_frames_.reserve(loop_frames); + for (const auto& frame_name : loop_frame_names) { + loop_frames_.emplace_back(LoadBitmap(frame_name)); } } void ScreenRecoveryUI::SetBackground(Icon icon) { std::lock_guard<std::mutex> lg(updateMutex); - currentIcon = icon; + current_icon_ = icon; update_screen_locked(); } @@ -845,7 +997,7 @@ void ScreenRecoveryUI::SetProgress(float fraction) { if (fraction > 1.0) fraction = 1.0; if (progressBarType == DETERMINATE && fraction > progress) { // Skip updates that aren't visibly different. - int width = gr_get_width(progressBarEmpty); + int width = gr_get_width(progress_bar_empty_.get()); float scale = width * progressScopeSize; if ((int)(progress * scale) != (int)(fraction * scale)) { progress = fraction; @@ -988,14 +1140,35 @@ void ScreenRecoveryUI::ShowFile(const std::string& filename) { text_row_ = old_text_row; } -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); +std::unique_ptr<Menu> ScreenRecoveryUI::CreateMenu( + const GRSurface* graphic_header, const std::vector<const GRSurface*>& graphic_items, + const std::vector<std::string>& text_headers, const std::vector<std::string>& text_items, + size_t initial_selection) const { + // horizontal unusable area: margin width + menu indent + size_t max_width = ScreenWidth() - margin_width_ - kMenuIndent; + // vertical unusable area: margin height + title lines + helper message + high light bar. + // It is safe to reserve more space. + size_t max_height = ScreenHeight() - margin_height_ - char_height_ * (title_lines_.size() + 3); + if (GraphicMenu::Validate(max_width, max_height, graphic_header, graphic_items)) { + return std::make_unique<GraphicMenu>(graphic_header, graphic_items, initial_selection, *this); + } + + fprintf(stderr, "Failed to initialize graphic menu, falling back to use the text menu.\n"); + + return CreateMenu(text_headers, text_items, initial_selection); +} + +std::unique_ptr<Menu> ScreenRecoveryUI::CreateMenu(const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const { if (text_rows_ > 0 && text_cols_ > 1) { - menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items, - initial_selection); - update_screen_locked(); + return std::make_unique<TextMenu>(scrollable_menu_, text_rows_, text_cols_ - 1, text_headers, + text_items, initial_selection, char_height_, *this); } + + fprintf(stderr, "Failed to create text menu, text_rows %zu, text_cols %zu.\n", text_rows_, + text_cols_); + return nullptr; } int ScreenRecoveryUI::SelectMenu(int sel) { @@ -1011,17 +1184,7 @@ int ScreenRecoveryUI::SelectMenu(int sel) { return sel; } -void ScreenRecoveryUI::EndMenu() { - std::lock_guard<std::mutex> lg(updateMutex); - if (menu_) { - menu_.reset(); - update_screen_locked(); - } -} - -size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers, - const std::vector<std::string>& items, size_t initial_selection, - bool menu_only, +size_t ScreenRecoveryUI::ShowMenu(std::unique_ptr<Menu>&& menu, bool menu_only, const std::function<int(int, bool)>& key_handler) { // Throw away keys pressed previously, so user doesn't accidentally trigger menu items. FlushKeys(); @@ -1030,9 +1193,13 @@ size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers, // menu. if (IsKeyInterrupted()) return static_cast<size_t>(KeyError::INTERRUPTED); - StartMenu(headers, items, initial_selection); + CHECK(menu != nullptr); - int selected = initial_selection; + // Starts and displays the menu + menu_ = std::move(menu); + Redraw(); + + int selected = menu_->selection(); int chosen_item = -1; while (chosen_item < 0) { int key = WaitKey(); @@ -1044,7 +1211,8 @@ size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers, continue; } else { LOG(INFO) << "Timed out waiting for key input; rebooting."; - EndMenu(); + menu_.reset(); + Redraw(); return static_cast<size_t>(KeyError::TIMED_OUT); } } @@ -1070,10 +1238,51 @@ size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers, } } - EndMenu(); + menu_.reset(); + Redraw(); + return chosen_item; } +size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers, + const std::vector<std::string>& items, size_t initial_selection, + bool menu_only, + const std::function<int(int, bool)>& key_handler) { + auto menu = CreateMenu(headers, items, initial_selection); + if (menu == nullptr) { + return initial_selection; + } + + return ShowMenu(CreateMenu(headers, items, initial_selection), menu_only, key_handler); +} + +size_t ScreenRecoveryUI::ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers, + const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) { + auto wipe_data_menu = CreateMenu(wipe_data_menu_header_text_.get(), + { try_again_text_.get(), factory_data_reset_text_.get() }, + backup_headers, backup_items, 0); + if (wipe_data_menu == nullptr) { + return 0; + } + + return ShowMenu(std::move(wipe_data_menu), true, key_handler); +} + +size_t ScreenRecoveryUI::ShowPromptWipeDataConfirmationMenu( + const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) { + auto confirmation_menu = + CreateMenu(wipe_data_confirmation_text_.get(), + { cancel_wipe_data_text_.get(), factory_data_reset_text_.get() }, backup_headers, + backup_items, 0); + if (confirmation_menu == nullptr) { + return 0; + } + + return ShowMenu(std::move(confirmation_menu), true, key_handler); +} + bool ScreenRecoveryUI::IsTextVisible() { std::lock_guard<std::mutex> lg(updateMutex); int visible = show_text; diff --git a/screen_ui.h b/screen_ui.h index f08f4f4f3..5cda2a2e5 100644 --- a/screen_ui.h +++ b/screen_ui.h @@ -29,25 +29,94 @@ #include "ui.h" // From minui/minui.h. -struct GRSurface; +class GRSurface; + +enum class UIElement { + HEADER, + MENU, + MENU_SEL_BG, + MENU_SEL_BG_ACTIVE, + MENU_SEL_FG, + LOG, + TEXT_FILL, + INFO +}; + +// Interface to draw the UI elements on the screen. +class DrawInterface { + public: + virtual ~DrawInterface() = default; + + // Sets the color to the predefined value for |element|. + virtual void SetColor(UIElement element) const = 0; + + // Draws a highlight bar at (x, y) - (x + width, y + height). + virtual void DrawHighlightBar(int x, int y, int width, int height) const = 0; + + // Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis. + virtual int DrawHorizontalRule(int y) const = 0; + + // Draws a line of text. Returns the offset it should be moving along Y-axis. + virtual int DrawTextLine(int x, int y, const std::string& line, bool bold) const = 0; + + // Draws surface portion (sx, sy, w, h) at screen location (dx, dy). + virtual void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, + int dy) const = 0; + + // Draws rectangle at (x, y) - (x + w, y + h). + virtual void DrawFill(int x, int y, int w, int h) const = 0; + + // Draws given surface (surface->pixel_bytes = 1) as text at (x, y). + virtual void DrawTextIcon(int x, int y, const GRSurface* surface) const = 0; + + // Draws multiple text lines. Returns the offset it should be moving along Y-axis. + virtual int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const = 0; + + // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It + // keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving + // along Y-axis. + virtual int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const = 0; +}; -// This class maintains the menu selection and display of the screen ui. +// 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,42 @@ 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 GRSurface's as the menu header and items. +class GraphicMenu : public Menu { + public: + // Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial + // selection to |initial_selection|. |headers| and |items| will be made local copies. + GraphicMenu(const GRSurface* graphic_headers, const std::vector<const GRSurface*>& graphic_items, + size_t initial_selection, const DrawInterface& draw_funcs); + + int Select(int sel) override; + int DrawHeader(int x, int y) const override; + int DrawItems(int x, int y, int screen_width, bool long_press) const override; + + // Checks if all the header and items are valid GRSurface's; and that they can fit in the area + // defined by |max_width| and |max_height|. + static bool Validate(size_t max_width, size_t max_height, const GRSurface* graphic_headers, + const std::vector<const GRSurface*>& graphic_items); + + // Returns true if |surface| fits on the screen with a vertical offset |y|. + static bool ValidateGraphicSurface(size_t max_width, size_t max_height, int y, + const GRSurface* surface); + + private: + // Menu headers and items in graphic icons. These are the copies owned by the class instance. + std::unique_ptr<GRSurface> graphic_headers_; + std::vector<std::unique_ptr<GRSurface>> graphic_items_; }; // Implementation of RecoveryUI appropriate for devices with a screen // (shows an icon + a progress bar, text logging, menu, etc.) -class ScreenRecoveryUI : public RecoveryUI { +class ScreenRecoveryUI : public RecoveryUI, public DrawInterface { public: - enum UIElement { - HEADER, - MENU, - MENU_SEL_BG, - MENU_SEL_BG_ACTIVE, - MENU_SEL_FG, - LOG, - TEXT_FILL, - INFO - }; - ScreenRecoveryUI(); explicit ScreenRecoveryUI(bool scrollable_menu); ~ScreenRecoveryUI() override; @@ -149,13 +231,23 @@ 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(); + // Displays the localized wipe data menu. + size_t ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers, + const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) override; + + // Displays the localized wipe data confirmation menu. + size_t ShowPromptWipeDataConfirmationMenu( + const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) override; + protected: + static constexpr int kMenuIndent = 4; + // The margin that we don't want to use for showing texts (e.g. round screen, or screen with // rounded corners). const int margin_width_; @@ -169,18 +261,31 @@ class ScreenRecoveryUI : public RecoveryUI { virtual bool InitTextParams(); - // Displays some header text followed by a menu of items, which appears at the top of the screen - // (in place of any scrolling ui_print() output, if necessary). - virtual void StartMenu(const std::vector<std::string>& headers, - const std::vector<std::string>& items, size_t initial_selection); + virtual bool LoadWipeDataMenuText(); + + // Creates a GraphicMenu with |graphic_header| and |graphic_items|. If the GraphicMenu isn't + // valid or it doesn't fit on the screen; falls back to create a TextMenu instead. If succeeds, + // returns a unique pointer to the created menu; otherwise returns nullptr. + virtual std::unique_ptr<Menu> CreateMenu(const GRSurface* graphic_header, + const std::vector<const GRSurface*>& graphic_items, + const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const; + + // Creates a TextMenu with |text_headers| and |text_items|; and sets the menu selection to + // |initial_selection|. + virtual std::unique_ptr<Menu> CreateMenu(const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const; + + // Takes the ownership of |menu| and displays it. + virtual size_t ShowMenu(std::unique_ptr<Menu>&& menu, bool menu_only, + const std::function<int(int, bool)>& key_handler); // Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item // selected. virtual int SelectMenu(int sel); - // Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed. - virtual void EndMenu(); - virtual void draw_background_locked(); virtual void draw_foreground_locked(); virtual void draw_screen_locked(); @@ -188,8 +293,8 @@ class ScreenRecoveryUI : public RecoveryUI { virtual void update_screen_locked(); virtual void update_progress_locked(); - GRSurface* GetCurrentFrame() const; - GRSurface* GetCurrentText() const; + const GRSurface* GetCurrentFrame() const; + const GRSurface* GetCurrentText() const; void ProgressThreadLoop(); @@ -199,8 +304,8 @@ class ScreenRecoveryUI : public RecoveryUI { void ClearText(); void LoadAnimation(); - void LoadBitmap(const char* filename, GRSurface** surface); - void LoadLocalizedBitmap(const char* filename, GRSurface** surface); + std::unique_ptr<GRSurface> LoadBitmap(const std::string& filename); + std::unique_ptr<GRSurface> LoadLocalizedBitmap(const std::string& filename); int PixelsFromDp(int dp) const; virtual int GetAnimationBaseline() const; @@ -212,44 +317,50 @@ 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; - - Icon currentIcon; + // Implementation of the draw functions in DrawInterface. + void SetColor(UIElement e) const override; + void DrawHighlightBar(int x, int y, int width, int height) const override; + int DrawHorizontalRule(int y) const override; + void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, + int dy) const override; + void DrawFill(int x, int y, int w, int h) const override; + void DrawTextIcon(int x, int y, const GRSurface* surface) const override; + int DrawTextLine(int x, int y, const std::string& line, bool bold) const override; + int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const override; + int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const override; // The layout to use. int layout_; - GRSurface* error_icon; - - GRSurface* erasing_text; - GRSurface* error_text; - GRSurface* installing_text; - GRSurface* no_command_text; - - GRSurface** introFrames; - GRSurface** loopFrames; - - GRSurface* progressBarEmpty; - GRSurface* progressBarFill; - GRSurface* stageMarkerEmpty; - GRSurface* stageMarkerFill; + // The images that contain localized texts. + std::unique_ptr<GRSurface> erasing_text_; + std::unique_ptr<GRSurface> error_text_; + std::unique_ptr<GRSurface> installing_text_; + std::unique_ptr<GRSurface> no_command_text_; + + // Localized text images for the wipe data menu. + std::unique_ptr<GRSurface> cancel_wipe_data_text_; + std::unique_ptr<GRSurface> factory_data_reset_text_; + std::unique_ptr<GRSurface> try_again_text_; + std::unique_ptr<GRSurface> wipe_data_confirmation_text_; + std::unique_ptr<GRSurface> wipe_data_menu_header_text_; + + std::unique_ptr<GRSurface> fastbootd_logo_; + + // current_icon_ points to one of the frames in intro_frames_ or loop_frames_, indexed by + // current_frame_, or error_icon_. + Icon current_icon_; + std::unique_ptr<GRSurface> error_icon_; + std::vector<std::unique_ptr<GRSurface>> intro_frames_; + std::vector<std::unique_ptr<GRSurface>> loop_frames_; + size_t current_frame_; + bool intro_done_; + + // progress_bar and stage_marker images. + std::unique_ptr<GRSurface> progress_bar_empty_; + std::unique_ptr<GRSurface> progress_bar_fill_; + std::unique_ptr<GRSurface> stage_marker_empty_; + std::unique_ptr<GRSurface> stage_marker_fill_; ProgressType progressBarType; @@ -279,13 +390,6 @@ class ScreenRecoveryUI : public RecoveryUI { std::thread progress_thread_; std::atomic<bool> progress_thread_stopped_{ false }; - // Number of intro frames and loop frames in the animation. - size_t intro_frames; - size_t loop_frames; - - size_t current_frame; - bool intro_done; - int stage, max_stage; int char_width_; @@ -68,6 +68,19 @@ class StubRecoveryUI : public RecoveryUI { return initial_selection; } + size_t ShowPromptWipeDataMenu(const std::vector<std::string>& /* backup_headers */, + const std::vector<std::string>& /* backup_items */, + const std::function<int(int, bool)>& /* key_handle */) override { + return 0; + } + + size_t ShowPromptWipeDataConfirmationMenu( + const std::vector<std::string>& /* backup_headers */, + const std::vector<std::string>& /* backup_items */, + const std::function<int(int, bool)>& /* key_handle */) override { + return 0; + } + void SetTitle(const std::vector<std::string>& /* lines */) override {} }; diff --git a/tests/Android.bp b/tests/Android.bp index ab4d31da2..898ed7d60 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -29,6 +29,7 @@ cc_defaults { "libcutils", "liblog", "libpng", + "libprocessgroup", "libselinux", "libz", "libziparchive", @@ -45,7 +46,7 @@ cc_defaults { static_libs: [ "libutils", ], - } + }, }, } @@ -92,13 +93,16 @@ librecovery_static_libs = [ "libhidl-gen-utils", "libhidlbase", "libhidltransport", - "libhwbinder", + "libhwbinder_noltopgo", + "libbinderthreadstate", + "liblp", "libvndksupport", "libtinyxml2", ] cc_test { name: "recovery_unit_test", + isolated: true, defaults: [ "recovery_test_defaults", @@ -117,7 +121,6 @@ cc_test { "libotautil", "libupdater", "libgtest_prod", - "libBionicGtestMain", ], data: ["testdata/*"], @@ -125,6 +128,7 @@ cc_test { cc_test { name: "recovery_manual_test", + isolated: true, defaults: [ "recovery_test_defaults", @@ -135,14 +139,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 +160,6 @@ cc_test { "libupdater", "libupdate_verifier", "libprotobuf-cpp-lite", - "libBionicGtestMain", ], data: [ @@ -170,6 +170,7 @@ cc_test { cc_test_host { name: "recovery_host_test", + isolated: true, defaults: [ "recovery_test_defaults", @@ -193,7 +194,6 @@ cc_test_host { "libdivsufsort64", "libdivsufsort", "libz", - "libBionicGtestMain", ], data: ["testdata/*"], diff --git a/tests/component/applypatch_modes_test.cpp b/tests/component/applypatch_modes_test.cpp index ce01f4fd5..08414b796 100644 --- a/tests/component/applypatch_modes_test.cpp +++ b/tests/component/applypatch_modes_test.cpp @@ -23,7 +23,6 @@ #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <bsdiff/bsdiff.h> #include <gtest/gtest.h> #include <openssl/sha.h> diff --git a/tests/component/bootloader_message_test.cpp b/tests/component/bootloader_message_test.cpp index 6cc59a495..b005d199c 100644 --- a/tests/component/bootloader_message_test.cpp +++ b/tests/component/bootloader_message_test.cpp @@ -17,8 +17,8 @@ #include <string> #include <vector> +#include <android-base/file.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <bootloader_message/bootloader_message.h> #include <gtest/gtest.h> diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp index cb4868a4a..e76ccbdfb 100644 --- a/tests/component/imgdiff_test.cpp +++ b/tests/component/imgdiff_test.cpp @@ -25,7 +25,6 @@ #include <android-base/memory.h> #include <android-base/stringprintf.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <applypatch/imgdiff.h> #include <applypatch/imgdiff_image.h> #include <applypatch/imgpatch.h> diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp index 08b429000..969805b42 100644 --- a/tests/component/install_test.cpp +++ b/tests/component/install_test.cpp @@ -20,13 +20,13 @@ #include <unistd.h> #include <algorithm> +#include <random> #include <string> #include <vector> #include <android-base/file.h> #include <android-base/properties.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> #include <vintf/VintfObjectRecovery.h> #include <ziparchive/zip_archive.h> @@ -36,15 +36,23 @@ #include "otautil/paths.h" #include "private/install.h" -TEST(InstallTest, verify_package_compatibility_no_entry) { - TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); +static void BuildZipArchive(const std::map<std::string, std::string>& file_map, int fd, + int compression_type) { + FILE* zip_file = fdopen(fd, "w"); ZipWriter writer(zip_file); - // The archive must have something to be opened correctly. - ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0)); - ASSERT_EQ(0, writer.FinishEntry()); + for (const auto& [name, content] : file_map) { + ASSERT_EQ(0, writer.StartEntry(name.c_str(), compression_type)); + ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size())); + ASSERT_EQ(0, writer.FinishEntry()); + } ASSERT_EQ(0, writer.Finish()); ASSERT_EQ(0, fclose(zip_file)); +} + +TEST(InstallTest, verify_package_compatibility_no_entry) { + TemporaryFile temp_file; + // The archive must have something to be opened correctly. + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); // Doesn't contain compatibility zip entry. ZipArchiveHandle zip; @@ -55,12 +63,7 @@ TEST(InstallTest, verify_package_compatibility_no_entry) { TEST(InstallTest, verify_package_compatibility_invalid_entry) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("compatibility.zip", 0)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { "compatibility.zip", "" } }, temp_file.release(), kCompressStored); // Empty compatibility zip entry. ZipArchiveHandle zip; @@ -71,77 +74,77 @@ TEST(InstallTest, verify_package_compatibility_invalid_entry) { TEST(InstallTest, read_metadata_from_package_smoke) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored)); - const std::string content("abcdefg"); - ASSERT_EQ(0, writer.WriteBytes(content.data(), content.size())); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + const std::string content("abc=defg"); + BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file.release(), + kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); - std::string metadata; - ASSERT_TRUE(read_metadata_from_package(zip, &metadata)); - ASSERT_EQ(content, metadata); + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ("defg", metadata["abc"]); CloseArchive(zip); TemporaryFile temp_file2; - FILE* zip_file2 = fdopen(temp_file2.release(), "w"); - ZipWriter writer2(zip_file2); - ASSERT_EQ(0, writer2.StartEntry("META-INF/com/android/metadata", kCompressDeflated)); - ASSERT_EQ(0, writer2.WriteBytes(content.data(), content.size())); - ASSERT_EQ(0, writer2.FinishEntry()); - ASSERT_EQ(0, writer2.Finish()); - ASSERT_EQ(0, fclose(zip_file2)); + BuildZipArchive({ { "META-INF/com/android/metadata", content } }, temp_file2.release(), + kCompressDeflated); ASSERT_EQ(0, OpenArchive(temp_file2.path, &zip)); metadata.clear(); - ASSERT_TRUE(read_metadata_from_package(zip, &metadata)); - ASSERT_EQ(content, metadata); + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ("defg", metadata["abc"]); CloseArchive(zip); } TEST(InstallTest, read_metadata_from_package_no_entry) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("dummy_entry", kCompressStored)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); - std::string metadata; - ASSERT_FALSE(read_metadata_from_package(zip, &metadata)); + std::map<std::string, std::string> metadata; + ASSERT_FALSE(ReadMetadataFromPackage(zip, &metadata)); CloseArchive(zip); } +TEST(InstallTest, read_wipe_ab_partition_list) { + std::vector<std::string> partition_list = { + "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b", + "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b", + "/dev/block/bootdevice/by-name/userdata", "# Wipe the boot partitions last", + "/dev/block/bootdevice/by-name/boot_a", "/dev/block/bootdevice/by-name/boot_b", + }; + TemporaryFile temp_file; + BuildZipArchive({ { "recovery.wipe", android::base::Join(partition_list, '\n') } }, + temp_file.release(), kCompressDeflated); + std::string wipe_package; + ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &wipe_package)); + + auto package = Package::CreateMemoryPackage( + std::vector<uint8_t>(wipe_package.begin(), wipe_package.end()), nullptr); + + auto read_partition_list = GetWipePartitionList(package.get()); + std::vector<std::string> expected = { + "/dev/block/bootdevice/by-name/system_a", "/dev/block/bootdevice/by-name/system_b", + "/dev/block/bootdevice/by-name/vendor_a", "/dev/block/bootdevice/by-name/vendor_b", + "/dev/block/bootdevice/by-name/userdata", "/dev/block/bootdevice/by-name/boot_a", + "/dev/block/bootdevice/by-name/boot_b", + }; + ASSERT_EQ(expected, read_partition_list); +} + TEST(InstallTest, verify_package_compatibility_with_libvintf_malformed_xml) { TemporaryFile compatibility_zip_file; - FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w"); - ZipWriter compatibility_zip_writer(compatibility_zip); - ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated)); std::string malformed_xml = "malformed"; - ASSERT_EQ(0, compatibility_zip_writer.WriteBytes(malformed_xml.data(), malformed_xml.size())); - ASSERT_EQ(0, compatibility_zip_writer.FinishEntry()); - ASSERT_EQ(0, compatibility_zip_writer.Finish()); - ASSERT_EQ(0, fclose(compatibility_zip)); + BuildZipArchive({ { "system_manifest.xml", malformed_xml } }, compatibility_zip_file.release(), + kCompressDeflated); TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored)); std::string compatibility_zip_content; ASSERT_TRUE( android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content)); - ASSERT_EQ(0, - writer.WriteBytes(compatibility_zip_content.data(), compatibility_zip_content.size())); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(), + kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); @@ -166,27 +169,15 @@ TEST(InstallTest, verify_package_compatibility_with_libvintf_system_manifest_xml ASSERT_TRUE( android::base::ReadFileToString(system_manifest_xml_path, &system_manifest_xml_content)); TemporaryFile compatibility_zip_file; - FILE* compatibility_zip = fdopen(compatibility_zip_file.release(), "w"); - ZipWriter compatibility_zip_writer(compatibility_zip); - ASSERT_EQ(0, compatibility_zip_writer.StartEntry("system_manifest.xml", kCompressDeflated)); - ASSERT_EQ(0, compatibility_zip_writer.WriteBytes(system_manifest_xml_content.data(), - system_manifest_xml_content.size())); - ASSERT_EQ(0, compatibility_zip_writer.FinishEntry()); - ASSERT_EQ(0, compatibility_zip_writer.Finish()); - ASSERT_EQ(0, fclose(compatibility_zip)); + BuildZipArchive({ { "system_manifest.xml", system_manifest_xml_content } }, + compatibility_zip_file.release(), kCompressDeflated); TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("compatibility.zip", kCompressStored)); std::string compatibility_zip_content; ASSERT_TRUE( android::base::ReadFileToString(compatibility_zip_file.path, &compatibility_zip_content)); - ASSERT_EQ(0, - writer.WriteBytes(compatibility_zip_content.data(), compatibility_zip_content.size())); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { "compatibility.zip", compatibility_zip_content } }, temp_file.release(), + kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); @@ -202,13 +193,8 @@ TEST(InstallTest, verify_package_compatibility_with_libvintf_system_manifest_xml TEST(InstallTest, SetUpNonAbUpdateCommands) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary"; - ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { UPDATE_BINARY_NAME, "" } }, temp_file.release(), kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); @@ -246,13 +232,8 @@ TEST(InstallTest, SetUpNonAbUpdateCommands) { TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); // The archive must have something to be opened correctly. - ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + BuildZipArchive({ { "dummy_entry", "" } }, temp_file.release(), kCompressStored); // Missing update binary. ZipArchiveHandle zip; @@ -268,16 +249,8 @@ TEST(InstallTest, SetUpNonAbUpdateCommands_MissingUpdateBinary) { static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored)); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.StartEntry("payload_properties.txt", kCompressStored)); + const std::string properties = "some_properties"; - ASSERT_EQ(0, writer.WriteBytes(properties.data(), properties.size())); - ASSERT_EQ(0, writer.FinishEntry()); - // A metadata entry is mandatory. - ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored)); std::string device = android::base::GetProperty("ro.product.device", ""); ASSERT_NE("", device); std::string timestamp = android::base::GetProperty("ro.build.date.utc", ""); @@ -288,21 +261,27 @@ static void VerifyAbUpdateCommands(const std::string& serialno, bool success = t if (!serialno.empty()) { meta.push_back("serialno=" + serialno); } - std::string metadata = android::base::Join(meta, "\n"); - ASSERT_EQ(0, writer.WriteBytes(metadata.data(), metadata.size())); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + std::string metadata_string = android::base::Join(meta, "\n"); + + BuildZipArchive({ { "payload.bin", "" }, + { "payload_properties.txt", properties }, + { "META-INF/com/android/metadata", metadata_string } }, + temp_file.release(), kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); ZipString payload_name("payload.bin"); ZipEntry payload_entry; ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry)); - int status_fd = 10; - std::string package = "/path/to/update.zip"; - std::vector<std::string> cmd; + + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); if (success) { + ASSERT_EQ(0, CheckPackageMetadata(metadata, OtaType::AB)); + + int status_fd = 10; + std::string package = "/path/to/update.zip"; + std::vector<std::string> cmd; ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); ASSERT_EQ(5U, cmd.size()); ASSERT_EQ("/system/bin/update_engine_sideload", cmd[0]); @@ -311,7 +290,7 @@ static void VerifyAbUpdateCommands(const std::string& serialno, bool success = t ASSERT_EQ("--headers=" + properties, cmd[3]); ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]); } else { - ASSERT_EQ(INSTALL_ERROR, SetUpAbUpdateCommands(package, zip, status_fd, &cmd)); + ASSERT_EQ(INSTALL_ERROR, CheckPackageMetadata(metadata, OtaType::AB)); } CloseArchive(zip); } @@ -323,13 +302,7 @@ TEST(InstallTest, SetUpAbUpdateCommands) { TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) { TemporaryFile temp_file; - FILE* zip_file = fdopen(temp_file.release(), "w"); - ZipWriter writer(zip_file); - // Missing payload_properties.txt. - ASSERT_EQ(0, writer.StartEntry("payload.bin", kCompressStored)); - ASSERT_EQ(0, writer.FinishEntry()); - // A metadata entry is mandatory. - ASSERT_EQ(0, writer.StartEntry("META-INF/com/android/metadata", kCompressStored)); + std::string device = android::base::GetProperty("ro.product.device", ""); ASSERT_NE("", device); std::string timestamp = android::base::GetProperty("ro.build.date.utc", ""); @@ -339,10 +312,13 @@ TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) { "ota-type=AB", "pre-device=" + device, "post-timestamp=" + timestamp, }, "\n"); - ASSERT_EQ(0, writer.WriteBytes(metadata.data(), metadata.size())); - ASSERT_EQ(0, writer.FinishEntry()); - ASSERT_EQ(0, writer.Finish()); - ASSERT_EQ(0, fclose(zip_file)); + + BuildZipArchive( + { + { "payload.bin", "" }, + { "META-INF/com/android/metadata", metadata }, + }, + temp_file.release(), kCompressStored); ZipArchiveHandle zip; ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); @@ -381,3 +357,241 @@ TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) { // String with the matching serialno should pass the verification. VerifyAbUpdateCommands(long_serialno); } + +static void test_check_package_metadata(const std::string& metadata_string, OtaType ota_type, + int exptected_result) { + TemporaryFile temp_file; + BuildZipArchive( + { + { "META-INF/com/android/metadata", metadata_string }, + }, + temp_file.release(), kCompressStored); + + ZipArchiveHandle zip; + ASSERT_EQ(0, OpenArchive(temp_file.path, &zip)); + + std::map<std::string, std::string> metadata; + ASSERT_TRUE(ReadMetadataFromPackage(zip, &metadata)); + ASSERT_EQ(exptected_result, CheckPackageMetadata(metadata, ota_type)); + CloseArchive(zip); +} + +TEST(InstallTest, CheckPackageMetadata_ota_type) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // ota-type must be present + std::string metadata = android::base::Join( + std::vector<std::string>{ + "pre-device=" + device, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); + + // Checks if ota-type matches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, 0); + + test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR); +} + +TEST(InstallTest, CheckPackageMetadata_device_type) { + // device type can not be empty + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR); + + // device type mismatches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=dummy_device_type", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR); +} + +TEST(InstallTest, CheckPackageMetadata_serial_number_smoke) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // Serial number doesn't need to exist + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, 0); + + // Serial number mismatches + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=dummy_serial", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR); + + std::string serialno = android::base::GetProperty("ro.serialno", ""); + ASSERT_NE("", serialno); + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + serialno, + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, 0); +} + +TEST(InstallTest, CheckPackageMetadata_multiple_serial_number) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string serialno = android::base::GetProperty("ro.serialno", ""); + ASSERT_NE("", serialno); + + std::vector<std::string> serial_numbers; + // Creates a dummy serial number string. + for (char c = 'a'; c <= 'z'; c++) { + serial_numbers.emplace_back(serialno.size(), c); + } + + // No matched serialno found. + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + android::base::Join(serial_numbers, '|'), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, INSTALL_ERROR); + + serial_numbers.emplace_back(serialno); + std::shuffle(serial_numbers.begin(), serial_numbers.end(), std::default_random_engine()); + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=BRICK", + "pre-device=" + device, + "serialno=" + android::base::Join(serial_numbers, '|'), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::BRICK, 0); +} + +TEST(InstallTest, CheckPackageMetadata_ab_build_version) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string build_version = android::base::GetProperty("ro.build.version.incremental", ""); + ASSERT_NE("", build_version); + + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build-incremental=" + build_version, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, 0); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build-incremental=dummy_build", + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); +} + +TEST(InstallTest, CheckPackageMetadata_ab_fingerprint) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + std::string finger_print = android::base::GetProperty("ro.build.fingerprint", ""); + ASSERT_NE("", finger_print); + + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build=" + finger_print, + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, 0); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "pre-build=dummy_build_fingerprint", + "post-timestamp=" + std::to_string(std::numeric_limits<int64_t>::max()), + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); +} + +TEST(InstallTest, CheckPackageMetadata_ab_post_timestamp) { + std::string device = android::base::GetProperty("ro.product.device", ""); + ASSERT_NE("", device); + + // post timestamp is required for upgrade. + std::string metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); + + // post timestamp should be larger than the timestamp on device. + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); + + // fingerprint is required for downgrade + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + "ota-downgrade=yes", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, INSTALL_ERROR); + + std::string finger_print = android::base::GetProperty("ro.build.fingerprint", ""); + ASSERT_NE("", finger_print); + + metadata = android::base::Join( + std::vector<std::string>{ + "ota-type=AB", + "pre-device=" + device, + "post-timestamp=0", + "pre-build=" + finger_print, + "ota-downgrade=yes", + }, + "\n"); + test_check_package_metadata(metadata, OtaType::AB, 0); +} diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp index 54329db22..d7fdb8fa0 100644 --- a/tests/component/resources_test.cpp +++ b/tests/component/resources_test.cpp @@ -101,7 +101,7 @@ TEST_P(ResourcesTest, ValidateLocale) { EXPECT_LT(0, len) << "Locale string should be non-empty."; EXPECT_NE(0, row[5]) << "Locale string is missing."; - ASSERT_GT(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file."; + ASSERT_GE(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file."; char* loc = reinterpret_cast<char*>(&row[5]); if (matches_locale(loc, kLocale.c_str())) { EXPECT_TRUE(android::base::StartsWith(loc, kLocale)); diff --git a/tests/component/sideload_test.cpp b/tests/component/sideload_test.cpp index b7109fcc2..d5e074c63 100644 --- a/tests/component/sideload_test.cpp +++ b/tests/component/sideload_test.cpp @@ -21,7 +21,6 @@ #include <android-base/file.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> #include "fuse_sideload.h" diff --git a/tests/component/uncrypt_test.cpp b/tests/component/uncrypt_test.cpp index 55baca2e3..e97d589a6 100644 --- a/tests/component/uncrypt_test.cpp +++ b/tests/component/uncrypt_test.cpp @@ -26,7 +26,6 @@ #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/properties.h> -#include <android-base/test_utils.h> #include <android-base/unique_fd.h> #include <bootloader_message/bootloader_message.h> #include <gtest/gtest.h> diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp index a97071635..e27e58c22 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> @@ -23,31 +24,54 @@ #include <android-base/file.h> #include <android-base/properties.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <google/protobuf/repeated_field.h> #include <gtest/gtest.h> #include "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,15 +83,22 @@ 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) { +TEST_F(UpdateVerifierTest, verify_image_text_format) { // This test relies on dm-verity support. if (!verity_supported) { GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; @@ -75,52 +106,58 @@ 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)); - - // 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(content, care_map_txt_)); + // CareMap in text format is no longer supported. + ASSERT_FALSE(verifier_.ParseCareMap()); } TEST_F(UpdateVerifierTest, verify_image_empty_care_map) { - ASSERT_FALSE(verify_image(care_map_file.path)); -} - -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\nline2\nline3", care_map_file.path)); - ASSERT_FALSE(verify_image(care_map_file.path)); + ASSERT_FALSE(verifier_.ParseCareMap()); } -TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) { +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) { // This test relies on dm-verity support. if (!verity_supported) { GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; return; } - std::string content = "system\n2,1,0"; - ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_file.path)); - ASSERT_FALSE(verify_image(care_map_file.path)); + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "name", "system" }, + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_TRUE(verifier_.ParseCareMap()); + ASSERT_TRUE(verifier_.VerifyPartitions()); } -TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) { +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) { // This test relies on dm-verity support. if (!verity_supported) { GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; return; } - std::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)); + std::vector<std::unordered_map<std::string, std::string>> partitions = { + { + { "ranges", "2,0,1" }, + { "id", property_id_ }, + { "fingerprint", fingerprint_ }, + }, + }; + + std::string proto = ConstructProto(partitions); + ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_)); + ASSERT_FALSE(verifier_.ParseCareMap()); } -TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) { +TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) { // This test relies on dm-verity support. if (!verity_supported) { GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support."; @@ -128,15 +165,20 @@ 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", "3,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_FALSE(verifier_.ParseCareMap()); } -TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) { +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."; @@ -144,15 +186,18 @@ TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) { } std::vector<std::unordered_map<std::string, std::string>> partitions = { - { { "ranges", "2,0,1" } }, + { + { "name", "system" }, + { "ranges", "2,0,1" }, + }, }; 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) { +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."; @@ -160,10 +205,15 @@ 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", "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/updater_test.cpp b/tests/component/updater_test.cpp index 24c63e776..a0a7b66ab 100644 --- a/tests/component/updater_test.cpp +++ b/tests/component/updater_test.cpp @@ -23,6 +23,7 @@ #include <algorithm> #include <memory> #include <string> +#include <string_view> #include <unordered_map> #include <vector> @@ -32,7 +33,6 @@ #include <android-base/properties.h> #include <android-base/stringprintf.h> #include <android-base/strings.h> -#include <android-base/test_utils.h> #include <bootloader_message/bootloader_message.h> #include <brotli/encode.h> #include <bsdiff/bsdiff.h> @@ -134,9 +134,9 @@ static void RunBlockImageUpdate(bool is_verify, const PackageEntries& entries, CloseArchive(handle); } -static std::string get_sha1(const std::string& content) { +static std::string GetSha1(std::string_view content) { uint8_t digest[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast<const uint8_t*>(content.c_str()), content.size(), digest); + SHA1(reinterpret_cast<const uint8_t*>(content.data()), content.size(), digest); return print_sha1(digest); } @@ -187,7 +187,7 @@ class UpdaterTest : public ::testing::Test { // Clear partition updated marker if any. std::string updated_marker{ temp_stash_base_.path }; - updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED"; + updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED"; ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker)); } @@ -223,14 +223,14 @@ TEST_F(UpdaterTest, patch_partition_check) { std::string source_content; ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content)); size_t source_size = source_content.size(); - std::string source_hash = get_sha1(source_content); + std::string source_hash = GetSha1(source_content); Partition source(source_file, source_size, source_hash); std::string target_file = from_testdata_base("recovery.img"); std::string target_content; ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content)); size_t target_size = target_content.size(); - std::string target_hash = get_sha1(target_content); + std::string target_hash = GetSha1(target_content); Partition target(target_file, target_size, target_hash); // One argument is not valid. @@ -619,54 +619,100 @@ TEST_F(UpdaterTest, block_image_update_parsing_error) { RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure); } -TEST_F(UpdaterTest, block_image_update_patch_data) { - std::string src_content = std::string(4096, 'a') + std::string(4096, 'c'); - std::string tgt_content = std::string(4096, 'b') + std::string(4096, 'd'); - +// Generates the bsdiff of the given source and target images, and writes the result entries. +// target_blocks specifies the block count to be written into the `bsdiff` command, which may be +// different from the given target size in order to trigger overrun / underrun paths. +static void GetEntriesForBsdiff(std::string_view source, std::string_view target, + size_t target_blocks, PackageEntries* entries) { // Generate the patch data. TemporaryFile patch_file; - ASSERT_EQ(0, - bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(), - reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(), - patch_file.path, nullptr)); + ASSERT_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(source.data()), source.size(), + reinterpret_cast<const uint8_t*>(target.data()), target.size(), + patch_file.path, nullptr)); std::string patch_content; ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content)); // Create the transfer list that contains a bsdiff. - std::string src_hash = get_sha1(src_content); - std::string tgt_hash = get_sha1(tgt_content); + std::string src_hash = GetSha1(source); + std::string tgt_hash = GetSha1(target); + size_t source_blocks = source.size() / 4096; std::vector<std::string> transfer_list{ // clang-format off "4", - "2", + std::to_string(target_blocks), "0", - "2", - "stash " + src_hash + " 2,0,2", - android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,2 2 - %s:2,0,2", patch_content.size(), - src_hash.c_str(), tgt_hash.c_str(), src_hash.c_str()), - "free " + src_hash, + "0", + // bsdiff patch_offset patch_length source_hash target_hash target_range source_block_count + // source_range + android::base::StringPrintf("bsdiff 0 %zu %s %s 2,0,%zu %zu 2,0,%zu", patch_content.size(), + src_hash.c_str(), tgt_hash.c_str(), target_blocks, source_blocks, + source_blocks), // clang-format on }; - PackageEntries entries{ + *entries = { { "new_data", "" }, { "patch_data", patch_content }, { "transfer_list", android::base::Join(transfer_list, '\n') }, }; +} - ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_)); - +TEST_F(UpdaterTest, block_image_update_patch_data) { + // Both source and target images have 10 blocks. + std::string source = + std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0'); + std::string target = + std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0'); + ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_)); + + PackageEntries entries; + GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2), + std::string_view(target).substr(0, 4096 * 2), 2, &entries); RunBlockImageUpdate(false, entries, image_file_, "t"); // The update_file should be patched correctly. - std::string updated_content; - ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content)); - ASSERT_EQ(tgt_content, updated_content); + std::string updated; + ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated)); + ASSERT_EQ(target, updated); +} + +TEST_F(UpdaterTest, block_image_update_patch_overrun) { + // Both source and target images have 10 blocks. + std::string source = + std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0'); + std::string target = + std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0'); + ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_)); + + // Provide one less block to trigger the overrun path. + PackageEntries entries; + GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2), + std::string_view(target).substr(0, 4096 * 2), 1, &entries); + + // The update should fail due to overrun. + RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure); +} + +TEST_F(UpdaterTest, block_image_update_patch_underrun) { + // Both source and target images have 10 blocks. + std::string source = + std::string(4096, 'a') + std::string(4096, 'c') + std::string(4096 * 3, '\0'); + std::string target = + std::string(4096, 'b') + std::string(4096, 'd') + std::string(4096 * 3, '\0'); + ASSERT_TRUE(android::base::WriteStringToFile(source, image_file_)); + + // Provide one more block to trigger the overrun path. + PackageEntries entries; + GetEntriesForBsdiff(std::string_view(source).substr(0, 4096 * 2), + std::string_view(target).substr(0, 4096 * 2), 3, &entries); + + // The update should fail due to underrun. + RunBlockImageUpdate(false, entries, image_file_, "", kPatchApplicationFailure); } TEST_F(UpdaterTest, block_image_update_fail) { std::string src_content(4096 * 2, 'e'); - std::string src_hash = get_sha1(src_content); + std::string src_hash = GetSha1(src_content); // Stash and free some blocks, then fail the update intentionally. std::vector<std::string> transfer_list{ // clang-format off @@ -692,7 +738,7 @@ TEST_F(UpdaterTest, block_image_update_fail) { RunBlockImageUpdate(false, entries, image_file_, ""); // Updater generates the stash name based on the input file name. - std::string name_digest = get_sha1(image_file_); + std::string name_digest = GetSha1(image_file_); std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest; ASSERT_EQ(0, access(stash_base.c_str(), F_OK)); // Expect the stashed blocks to be freed. @@ -796,9 +842,9 @@ TEST_F(UpdaterTest, last_command_update) { std::string block1(4096, '1'); std::string block2(4096, '2'); std::string block3(4096, '3'); - std::string block1_hash = get_sha1(block1); - std::string block2_hash = get_sha1(block2); - std::string block3_hash = get_sha1(block3); + std::string block1_hash = GetSha1(block1); + std::string block2_hash = GetSha1(block2); + std::string block3_hash = GetSha1(block3); // Compose the transfer list to fail the first update. std::vector<std::string> transfer_list_fail{ @@ -864,8 +910,8 @@ TEST_F(UpdaterTest, last_command_update) { TEST_F(UpdaterTest, last_command_update_unresumable) { std::string block1(4096, '1'); std::string block2(4096, '2'); - std::string block1_hash = get_sha1(block1); - std::string block2_hash = get_sha1(block2); + std::string block1_hash = GetSha1(block1); + std::string block2_hash = GetSha1(block2); // Construct an unresumable update with source blocks mismatch. std::vector<std::string> transfer_list_unresumable{ @@ -901,9 +947,9 @@ TEST_F(UpdaterTest, last_command_verify) { std::string block1(4096, '1'); std::string block2(4096, '2'); std::string block3(4096, '3'); - std::string block1_hash = get_sha1(block1); - std::string block2_hash = get_sha1(block2); - std::string block3_hash = get_sha1(block3); + std::string block1_hash = GetSha1(block1); + std::string block2_hash = GetSha1(block2); + std::string block3_hash = GetSha1(block3); std::vector<std::string> transfer_list_verify{ // clang-format off @@ -972,7 +1018,7 @@ class ResumableUpdaterTest : public testing::TestWithParam<size_t> { // Clear partition updated marker if any. std::string updated_marker{ temp_stash_base_.path }; - updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED"; + updated_marker += "/" + GetSha1(image_temp_file_.path) + ".UPDATED"; ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker)); } @@ -1003,10 +1049,10 @@ static std::vector<std::string> GenerateTransferList() { std::string i(4096, 'i'); std::string zero(4096, '\0'); - std::string a_hash = get_sha1(a); - std::string b_hash = get_sha1(b); - std::string c_hash = get_sha1(c); - std::string e_hash = get_sha1(e); + std::string a_hash = GetSha1(a); + std::string b_hash = GetSha1(b); + std::string c_hash = GetSha1(c); + std::string e_hash = GetSha1(e); auto loc = [](const std::string& range_text) { std::vector<std::string> pieces = android::base::Split(range_text, "-"); @@ -1027,8 +1073,8 @@ static std::vector<std::string> GenerateTransferList() { // patch 1: "b d c" -> "g" TemporaryFile patch_file_bdc_g; std::string bdc = b + d + c; - std::string bdc_hash = get_sha1(bdc); - std::string g_hash = get_sha1(g); + std::string bdc_hash = GetSha1(bdc); + std::string g_hash = GetSha1(g); CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(), reinterpret_cast<const uint8_t*>(g.data()), g.size(), patch_file_bdc_g.path, nullptr)); @@ -1038,9 +1084,9 @@ static std::vector<std::string> GenerateTransferList() { // patch 2: "a b c d" -> "d c b" TemporaryFile patch_file_abcd_dcb; std::string abcd = a + b + c + d; - std::string abcd_hash = get_sha1(abcd); + std::string abcd_hash = GetSha1(abcd); std::string dcb = d + c + b; - std::string dcb_hash = get_sha1(dcb); + std::string dcb_hash = GetSha1(dcb); CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(), reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(), patch_file_abcd_dcb.path, nullptr)); diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp index 3246ecdbc..c26d76d73 100644 --- a/tests/component/verifier_test.cpp +++ b/tests/component/verifier_test.cpp @@ -26,32 +26,232 @@ #include <android-base/file.h> #include <android-base/stringprintf.h> -#include <android-base/test_utils.h> +#include <android-base/strings.h> +#include <android-base/unique_fd.h> #include <gtest/gtest.h> +#include <openssl/bn.h> +#include <openssl/ec.h> +#include <openssl/nid.h> +#include <ziparchive/zip_writer.h> #include "common/test_constants.h" #include "otautil/sysutil.h" +#include "package.h" #include "verifier.h" using namespace std::string_literals; +static void LoadKeyFromFile(const std::string& file_name, Certificate* cert) { + std::string testkey_string; + ASSERT_TRUE(android::base::ReadFileToString(file_name, &testkey_string)); + ASSERT_TRUE(LoadCertificateFromBuffer( + std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), cert)); +} + +static void VerifyFile(const std::string& content, const std::vector<Certificate>& keys, + int expected) { + auto package = + Package::CreateMemoryPackage(std::vector<uint8_t>(content.begin(), content.end()), nullptr); + ASSERT_NE(nullptr, package); + + ASSERT_EQ(expected, verify_file(package.get(), keys)); +} + +static void VerifyPackageWithCertificates(const std::string& name, + const std::vector<Certificate>& certs) { + std::string path = from_testdata_base(name); + auto package = Package::CreateMemoryPackage(path, nullptr); + ASSERT_NE(nullptr, package); + + ASSERT_EQ(VERIFY_SUCCESS, verify_file(package.get(), certs)); +} + +static void VerifyPackageWithSingleCertificate(const std::string& name, Certificate&& cert) { + std::vector<Certificate> certs; + certs.emplace_back(std::move(cert)); + VerifyPackageWithCertificates(name, certs); +} + +static void BuildCertificateArchive(const std::vector<std::string>& file_names, int fd) { + FILE* zip_file_ptr = fdopen(fd, "wb"); + ZipWriter zip_writer(zip_file_ptr); + + for (const auto& name : file_names) { + std::string content; + ASSERT_TRUE(android::base::ReadFileToString(name, &content)); + + // Makes sure the zip entry name has the correct suffix. + std::string entry_name = name; + if (!android::base::EndsWith(entry_name, "x509.pem")) { + entry_name += "x509.pem"; + } + ASSERT_EQ(0, zip_writer.StartEntry(entry_name.c_str(), ZipWriter::kCompress)); + ASSERT_EQ(0, zip_writer.WriteBytes(content.data(), content.size())); + ASSERT_EQ(0, zip_writer.FinishEntry()); + } + + ASSERT_EQ(0, zip_writer.Finish()); + ASSERT_EQ(0, fclose(zip_file_ptr)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_failure) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + std::string testkey_string; + ASSERT_TRUE( + android::base::ReadFileToString(from_testdata_base("testkey_v1.txt"), &testkey_string)); + ASSERT_FALSE(LoadCertificateFromBuffer( + std::vector<uint8_t>(testkey_string.begin(), testkey_string.end()), &cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent3) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v1.x509.pem"), &cert); + + ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v1.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha1_exponent65537) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v2.x509.pem"), &cert); + + ASSERT_EQ(SHA_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v2.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent3) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v3.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_exponent65537) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v4.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_RSA, cert.key_type); + ASSERT_EQ(nullptr, cert.ec); + + VerifyPackageWithSingleCertificate("otasigned_v4.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_sha256_ec256bits) { + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v5.x509.pem"), &cert); + + ASSERT_EQ(SHA256_DIGEST_LENGTH, cert.hash_len); + ASSERT_EQ(Certificate::KEY_TYPE_EC, cert.key_type); + ASSERT_EQ(nullptr, cert.rsa); + + VerifyPackageWithSingleCertificate("otasigned_v5.zip", std::move(cert)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_check_rsa_keys) { + std::unique_ptr<RSA, RSADeleter> rsa(RSA_new()); + std::unique_ptr<BIGNUM, decltype(&BN_free)> exponent(BN_new(), BN_free); + BN_set_word(exponent.get(), 3); + RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr); + ASSERT_TRUE(CheckRSAKey(rsa)); + + // Exponent is expected to be 3 or 65537 + BN_set_word(exponent.get(), 17); + RSA_generate_key_ex(rsa.get(), 2048, exponent.get(), nullptr); + ASSERT_FALSE(CheckRSAKey(rsa)); + + // Modulus is expected to be 2048. + BN_set_word(exponent.get(), 3); + RSA_generate_key_ex(rsa.get(), 1024, exponent.get(), nullptr); + ASSERT_FALSE(CheckRSAKey(rsa)); +} + +TEST(VerifierTest, LoadCertificateFromBuffer_check_ec_keys) { + std::unique_ptr<EC_KEY, ECKEYDeleter> ec(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + ASSERT_EQ(1, EC_KEY_generate_key(ec.get())); + ASSERT_TRUE(CheckECKey(ec)); + + // Expects 256-bit EC key with curve NIST P-256 + ec.reset(EC_KEY_new_by_curve_name(NID_secp224r1)); + ASSERT_EQ(1, EC_KEY_generate_key(ec.get())); + ASSERT_FALSE(CheckECKey(ec)); +} + +TEST(VerifierTest, LoadKeysFromZipfile_empty_archive) { + TemporaryFile otacerts; + BuildCertificateArchive({}, otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_TRUE(certs.empty()); +} + +TEST(VerifierTest, LoadKeysFromZipfile_single_key) { + TemporaryFile otacerts; + BuildCertificateArchive({ from_testdata_base("testkey_v1.x509.pem") }, otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(1, certs.size()); + + VerifyPackageWithCertificates("otasigned_v1.zip", certs); +} + +TEST(VerifierTest, LoadKeysFromZipfile_corrupted_key) { + TemporaryFile corrupted_key; + std::string content; + ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v1.x509.pem"), &content)); + content = "random-contents" + content; + ASSERT_TRUE(android::base::WriteStringToFd(content, corrupted_key.release())); + + TemporaryFile otacerts; + BuildCertificateArchive({ from_testdata_base("testkey_v2.x509.pem"), corrupted_key.path }, + otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(0, certs.size()); +} + +TEST(VerifierTest, LoadKeysFromZipfile_multiple_key) { + TemporaryFile otacerts; + BuildCertificateArchive( + { + from_testdata_base("testkey_v3.x509.pem"), + from_testdata_base("testkey_v4.x509.pem"), + from_testdata_base("testkey_v5.x509.pem"), + + }, + otacerts.release()); + std::vector<Certificate> certs = LoadKeysFromZipfile(otacerts.path); + ASSERT_EQ(3, certs.size()); + + VerifyPackageWithCertificates("otasigned_v3.zip", certs); + VerifyPackageWithCertificates("otasigned_v4.zip", certs); + VerifyPackageWithCertificates("otasigned_v5.zip", certs); +} + class VerifierTest : public testing::TestWithParam<std::vector<std::string>> { protected: void SetUp() override { std::vector<std::string> args = GetParam(); - std::string package = from_testdata_base(args[0]); - if (!memmap.MapFile(package)) { - FAIL() << "Failed to mmap " << package << ": " << strerror(errno) << "\n"; - } + std::string path = from_testdata_base(args[0]); + package_ = Package::CreateMemoryPackage(path, nullptr); + ASSERT_NE(nullptr, package_); for (auto it = ++args.cbegin(); it != args.cend(); ++it) { - std::string public_key_file = from_testdata_base("testkey_" + *it + ".txt"); - ASSERT_TRUE(load_keys(public_key_file.c_str(), certs)); + std::string public_key_file = from_testdata_base("testkey_" + *it + ".x509.pem"); + certs_.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(public_key_file, &certs_.back()); } } - MemMapping memmap; - std::vector<Certificate> certs; + std::unique_ptr<Package> package_; + std::vector<Certificate> certs_; }; class VerifierSuccessTest : public VerifierTest { @@ -60,70 +260,10 @@ class VerifierSuccessTest : public VerifierTest { class VerifierFailureTest : public VerifierTest { }; -TEST(VerifierTest, load_keys_multiple_keys) { - std::string testkey_v4; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v4.txt"), &testkey_v4)); - - std::string testkey_v3; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3)); - - std::string keys = testkey_v4 + "," + testkey_v3 + "," + testkey_v4; - TemporaryFile key_file1; - ASSERT_TRUE(android::base::WriteStringToFile(keys, key_file1.path)); - std::vector<Certificate> certs; - ASSERT_TRUE(load_keys(key_file1.path, certs)); - ASSERT_EQ(3U, certs.size()); -} - -TEST(VerifierTest, load_keys_invalid_keys) { - std::vector<Certificate> certs; - ASSERT_FALSE(load_keys("/doesntexist", certs)); - - // Empty file. - TemporaryFile key_file1; - ASSERT_FALSE(load_keys(key_file1.path, certs)); - - // Invalid contents. - ASSERT_TRUE(android::base::WriteStringToFile("invalid", key_file1.path)); - ASSERT_FALSE(load_keys(key_file1.path, certs)); - - std::string testkey_v4; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v4.txt"), &testkey_v4)); - - // Invalid key version: "v4 ..." => "v6 ...". - std::string invalid_key2(testkey_v4); - invalid_key2[1] = '6'; - TemporaryFile key_file2; - ASSERT_TRUE(android::base::WriteStringToFile(invalid_key2, key_file2.path)); - ASSERT_FALSE(load_keys(key_file2.path, certs)); - - // Invalid key content: inserted extra bytes ",2209831334". - std::string invalid_key3(testkey_v4); - invalid_key3.insert(invalid_key2.size() - 2, ",2209831334"); - TemporaryFile key_file3; - ASSERT_TRUE(android::base::WriteStringToFile(invalid_key3, key_file3.path)); - ASSERT_FALSE(load_keys(key_file3.path, certs)); - - // Invalid key: the last key must not end with an extra ','. - std::string invalid_key4 = testkey_v4 + ","; - TemporaryFile key_file4; - ASSERT_TRUE(android::base::WriteStringToFile(invalid_key4, key_file4.path)); - ASSERT_FALSE(load_keys(key_file4.path, certs)); - - // Invalid key separator. - std::string invalid_key5 = testkey_v4 + ";" + testkey_v4; - TemporaryFile key_file5; - ASSERT_TRUE(android::base::WriteStringToFile(invalid_key5, key_file5.path)); - ASSERT_FALSE(load_keys(key_file5.path, certs)); -} - TEST(VerifierTest, BadPackage_AlteredFooter) { - std::string testkey_v3; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3)); - TemporaryFile key_file1; - ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file1.path)); std::vector<Certificate> certs; - ASSERT_TRUE(load_keys(key_file1.path, certs)); + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); std::string package; ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package)); @@ -131,18 +271,13 @@ TEST(VerifierTest, BadPackage_AlteredFooter) { // Alter the footer. package[package.size() - 5] = '\x05'; - ASSERT_EQ(VERIFY_FAILURE, - verify_file(reinterpret_cast<const unsigned char*>(package.data()), package.size(), - certs)); + VerifyFile(package, certs, VERIFY_FAILURE); } TEST(VerifierTest, BadPackage_AlteredContent) { - std::string testkey_v3; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3)); - TemporaryFile key_file1; - ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file1.path)); std::vector<Certificate> certs; - ASSERT_TRUE(load_keys(key_file1.path, certs)); + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); std::string package; ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("otasigned_v3.zip"), &package)); @@ -151,38 +286,29 @@ TEST(VerifierTest, BadPackage_AlteredContent) { // Alter the content. std::string altered1(package); altered1[50] += 1; - ASSERT_EQ(VERIFY_FAILURE, - verify_file(reinterpret_cast<const unsigned char*>(altered1.data()), altered1.size(), - certs)); + VerifyFile(altered1, certs, VERIFY_FAILURE); std::string altered2(package); altered2[10] += 1; - ASSERT_EQ(VERIFY_FAILURE, - verify_file(reinterpret_cast<const unsigned char*>(altered2.data()), altered2.size(), - certs)); + VerifyFile(altered2, certs, VERIFY_FAILURE); } TEST(VerifierTest, BadPackage_SignatureStartOutOfBounds) { - std::string testkey_v3; - ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("testkey_v3.txt"), &testkey_v3)); - - TemporaryFile key_file; - ASSERT_TRUE(android::base::WriteStringToFile(testkey_v3, key_file.path)); std::vector<Certificate> certs; - ASSERT_TRUE(load_keys(key_file.path, certs)); + certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + LoadKeyFromFile(from_testdata_base("testkey_v3.x509.pem"), &certs.back()); // Signature start is 65535 (0xffff) while comment size is 0 (Bug: 31914369). std::string package = "\x50\x4b\x05\x06"s + std::string(12, '\0') + "\xff\xff\xff\xff\x00\x00"s; - ASSERT_EQ(VERIFY_FAILURE, verify_file(reinterpret_cast<const unsigned char*>(package.data()), - package.size(), certs)); + VerifyFile(package, certs, VERIFY_FAILURE); } TEST_P(VerifierSuccessTest, VerifySucceed) { - ASSERT_EQ(verify_file(memmap.addr, memmap.length, certs, nullptr), VERIFY_SUCCESS); + ASSERT_EQ(VERIFY_SUCCESS, verify_file(package_.get(), certs_)); } TEST_P(VerifierFailureTest, VerifyFailure) { - ASSERT_EQ(verify_file(memmap.addr, memmap.length, certs, nullptr), VERIFY_FAILURE); + ASSERT_EQ(VERIFY_FAILURE, verify_file(package_.get(), certs_)); } INSTANTIATE_TEST_CASE_P(SingleKeySuccess, VerifierSuccessTest, diff --git a/tests/testdata/battery_scale.png b/tests/testdata/battery_scale.png Binary files differnew file mode 100644 index 000000000..2ae8f0fd7 --- /dev/null +++ b/tests/testdata/battery_scale.png diff --git a/tests/testdata/jarsigned.zip b/tests/testdata/jarsigned.zip Binary files differdeleted file mode 100644 index 8b1ef8bdd..000000000 --- a/tests/testdata/jarsigned.zip +++ /dev/null diff --git a/tests/testdata/patch.bsdiff b/tests/testdata/patch.bsdiff Binary files differdeleted file mode 100644 index b78d38573..000000000 --- a/tests/testdata/patch.bsdiff +++ /dev/null diff --git a/tests/testdata/unsigned.zip b/tests/testdata/unsigned.zip Binary files differdeleted file mode 100644 index 24e3eadac..000000000 --- a/tests/testdata/unsigned.zip +++ /dev/null diff --git a/tests/unit/applypatch_test.cpp b/tests/unit/applypatch_test.cpp index 066f981b4..794f2c103 100644 --- a/tests/unit/applypatch_test.cpp +++ b/tests/unit/applypatch_test.cpp @@ -32,7 +32,6 @@ #include <android-base/file.h> #include <android-base/logging.h> #include <android-base/stringprintf.h> -#include <android-base/test_utils.h> #include <android-base/unique_fd.h> #include <gtest/gtest.h> diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp index 1ca786c28..4dd111a70 100644 --- a/tests/unit/dirutil_test.cpp +++ b/tests/unit/dirutil_test.cpp @@ -20,7 +20,7 @@ #include <string> -#include <android-base/test_utils.h> +#include <android-base/file.h> #include <gtest/gtest.h> #include "otautil/dirutil.h" diff --git a/tests/unit/minui_test.cpp b/tests/unit/minui_test.cpp new file mode 100644 index 000000000..c7d7f7eef --- /dev/null +++ b/tests/unit/minui_test.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <stdint.h> +#include <stdlib.h> + +#include <limits> +#include <vector> + +#include <gtest/gtest.h> + +#include "minui/minui.h" + +TEST(GRSurfaceTest, Create_aligned) { + auto surface = GRSurface::Create(9, 11, 9, 1); + ASSERT_TRUE(surface); + ASSERT_EQ(0, reinterpret_cast<uintptr_t>(surface->data()) % GRSurface::kSurfaceDataAlignment); + // data_size will be rounded up to the next multiple of GRSurface::kSurfaceDataAlignment. + ASSERT_EQ(0, surface->data_size() % GRSurface::kSurfaceDataAlignment); + ASSERT_GE(surface->data_size(), 11 * 9); +} + +TEST(GRSurfaceTest, Create_invalid_inputs) { + ASSERT_FALSE(GRSurface::Create(9, 11, 0, 1)); + ASSERT_FALSE(GRSurface::Create(9, 0, 9, 1)); + ASSERT_FALSE(GRSurface::Create(0, 11, 9, 1)); + ASSERT_FALSE(GRSurface::Create(9, 11, 9, 0)); + ASSERT_FALSE(GRSurface::Create(9, 101, std::numeric_limits<size_t>::max() / 100, 1)); +} + +TEST(GRSurfaceTest, Clone) { + auto image = GRSurface::Create(50, 10, 50, 1); + ASSERT_GE(image->data_size(), 10 * 50); + for (auto i = 0; i < image->data_size(); i++) { + image->data()[i] = rand() % 128; + } + auto image_copy = image->Clone(); + ASSERT_EQ(image->data_size(), image_copy->data_size()); + ASSERT_EQ(std::vector(image->data(), image->data() + image->data_size()), + std::vector(image_copy->data(), image_copy->data() + image->data_size())); +} diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp new file mode 100644 index 000000000..72169a0c6 --- /dev/null +++ b/tests/unit/parse_install_logs_test.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <map> +#include <string> +#include <vector> + +#include <android-base/file.h> +#include <android-base/strings.h> +#include <gtest/gtest.h> + +#include "otautil/parse_install_logs.h" + +TEST(ParseInstallLogsTest, EmptyFile) { + TemporaryFile last_install; + + auto metrics = ParseLastInstall(last_install.path); + ASSERT_TRUE(metrics.empty()); +} + +TEST(ParseInstallLogsTest, SideloadSmoke) { + TemporaryFile last_install; + ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path)); + auto metrics = ParseLastInstall(last_install.path); + ASSERT_EQ(metrics.end(), metrics.find("ota_sideload")); + + ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path)); + metrics = ParseLastInstall(last_install.path); + ASSERT_NE(metrics.end(), metrics.find("ota_sideload")); +} + +TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) { + std::vector<std::string> lines = { + "/sideload/package.zip", + "0", + "time_total: 300", + "uncrypt_time: 40", + "source_build: 4973410", + "bytes_written_system: " + std::to_string(1200 * 1024 * 1024), + "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024), + "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024), + "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024), + "temperature_start: 37000", + "temperature_end: 38000", + "temperature_max: 39000", + "error: 22", + "cause: 55", + }; + + auto metrics = ParseRecoveryUpdateMetrics(lines); + + std::map<std::string, int64_t> expected_result = { + { "ota_time_total", 300 }, { "ota_uncrypt_time", 40 }, + { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 }, + { "ota_stashed_in_MiBs", 350 }, { "ota_temperature_start", 37000 }, + { "ota_temperature_end", 38000 }, { "ota_temperature_max", 39000 }, + { "ota_non_ab_error_code", 22 }, { "ota_non_ab_cause_code", 55 }, + }; + + ASSERT_EQ(expected_result, metrics); +} diff --git a/tests/unit/resources_test.cpp b/tests/unit/resources_test.cpp new file mode 100644 index 000000000..c3f72718f --- /dev/null +++ b/tests/unit/resources_test.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> + +#include <gtest/gtest.h> + +#include "common/test_constants.h" +#include "minui/minui.h" + +TEST(ResourcesTest, res_create_multi_display_surface) { + GRSurface** frames; + int frame_count; + int fps; + ASSERT_EQ(0, res_create_multi_display_surface(from_testdata_base("battery_scale.png").c_str(), + &frame_count, &fps, &frames)); + ASSERT_EQ(6, frame_count); + ASSERT_EQ(20, fps); + + for (auto i = 0; i < frame_count; i++) { + free(frames[i]); + } + free(frames); +} diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp index 7d97a006b..647c7b2d3 100644 --- a/tests/unit/screen_ui_test.cpp +++ b/tests/unit/screen_ui_test.cpp @@ -23,10 +23,11 @@ #include <string> #include <vector> +#include <android-base/file.h> #include <android-base/logging.h> #include <android-base/stringprintf.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> +#include <gtest/gtest_prod.h> #include "common/test_constants.h" #include "device.h" @@ -38,8 +39,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(const GRSurface* /* surface */, int /* sx */, int /* sy */, int /* w */, + int /* h */, int /* dx */, int /* dy */) const override {} + void DrawFill(int /* x */, int /* y */, int /* w */, int /* h */) const override {} + void DrawTextIcon(int /* x */, int /* y */, const GRSurface* /* surface */) const override {} + int DrawTextLines(int /* x */, int /* y */, + const std::vector<std::string>& /* lines */) const override { + return 0; + } + int DrawWrappedTextLines(int /* x */, int /* y */, + const std::vector<std::string>& /* lines */) const override { + return 0; + } +}; + +class ScreenUITest : public testing::Test { + protected: + MockDrawFunctions draw_funcs_; +}; + +TEST_F(ScreenUITest, StartPhoneMenuSmoke) { + TextMenu menu(false, 10, 20, HEADERS, ITEMS, 0, 20, draw_funcs_); ASSERT_FALSE(menu.scrollable()); ASSERT_EQ(HEADERS[0], menu.text_headers()[0]); ASSERT_EQ(5u, menu.ItemsCount()); @@ -53,8 +85,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 +101,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 +116,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 +133,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 +162,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 +191,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. @@ -198,6 +230,53 @@ TEST(ScreenUITest, WearMenuSelectItemsOverflow) { ASSERT_EQ(3u, menu.MenuEnd()); } +TEST_F(ScreenUITest, GraphicMenuSelection) { + auto image = GRSurface::Create(50, 50, 50, 1); + auto header = image->Clone(); + std::vector<const GRSurface*> items = { + image.get(), + image.get(), + image.get(), + }; + GraphicMenu menu(header.get(), items, 0, draw_funcs_); + + ASSERT_EQ(0, menu.selection()); + + int sel = 0; + for (int i = 0; i < 3; i++) { + sel = menu.Select(++sel); + ASSERT_EQ((i + 1) % 3, sel); + ASSERT_EQ(sel, menu.selection()); + } + + sel = 0; + for (int i = 0; i < 3; i++) { + sel = menu.Select(--sel); + ASSERT_EQ(2 - i, sel); + ASSERT_EQ(sel, menu.selection()); + } +} + +TEST_F(ScreenUITest, GraphicMenuValidate) { + auto image = GRSurface::Create(50, 50, 50, 1); + auto header = image->Clone(); + std::vector<const GRSurface*> items = { + image.get(), + image.get(), + image.get(), + }; + + ASSERT_TRUE(GraphicMenu::Validate(200, 200, header.get(), items)); + + // Menu exceeds the horizontal boundary. + auto wide_surface = GRSurface::Create(300, 50, 300, 1); + ASSERT_FALSE(GraphicMenu::Validate(299, 200, wide_surface.get(), items)); + + // Menu exceeds the vertical boundary. + items.emplace_back(image.get()); + ASSERT_FALSE(GraphicMenu::Validate(200, 249, header.get(), items)); +} + static constexpr int kMagicAction = 101; enum class KeyCode : int { @@ -228,24 +307,13 @@ class TestableScreenRecoveryUI : public ScreenRecoveryUI { int KeyHandler(int key, bool visible) const; - // The following functions expose the protected members for test purpose. - void RunLoadAnimation() { - LoadAnimation(); - } - - size_t GetLoopFrames() const { - return loop_frames; - } - - size_t GetIntroFrames() const { - return intro_frames; - } - - bool GetRtlLocale() const { - return rtl_locale_; - } - private: + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, Init); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocale); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation); + FRIEND_TEST(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation); + std::vector<KeyCode> key_buffer_; size_t key_buffer_index_; }; @@ -272,7 +340,7 @@ int TestableScreenRecoveryUI::WaitKey() { return static_cast<int>(key_buffer_[key_buffer_index_++]); } -class ScreenRecoveryUITest : public ::testing::Test { +class DISABLED_ScreenRecoveryUITest : public ::testing::Test { protected: const std::string kTestLocale = "en-US"; const std::string kTestRtlLocale = "ar"; @@ -304,22 +372,22 @@ class ScreenRecoveryUITest : public ::testing::Test { } \ } while (false) -TEST_F(ScreenRecoveryUITest, Init) { +TEST_F(DISABLED_ScreenRecoveryUITest, Init) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); ASSERT_EQ(kTestLocale, ui_->GetLocale()); - ASSERT_FALSE(ui_->GetRtlLocale()); + ASSERT_FALSE(ui_->rtl_locale_); ASSERT_FALSE(ui_->IsTextVisible()); ASSERT_FALSE(ui_->WasTextEverVisible()); } -TEST_F(ScreenRecoveryUITest, dtor_NotCallingInit) { +TEST_F(DISABLED_ScreenRecoveryUITest, dtor_NotCallingInit) { ui_.reset(); ASSERT_FALSE(ui_); } -TEST_F(ScreenRecoveryUITest, ShowText) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowText) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -333,21 +401,21 @@ TEST_F(ScreenRecoveryUITest, ShowText) { ASSERT_TRUE(ui_->WasTextEverVisible()); } -TEST_F(ScreenRecoveryUITest, RtlLocale) { +TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocale) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestRtlLocale)); - ASSERT_TRUE(ui_->GetRtlLocale()); + ASSERT_TRUE(ui_->rtl_locale_); } -TEST_F(ScreenRecoveryUITest, RtlLocaleWithSuffix) { +TEST_F(DISABLED_ScreenRecoveryUITest, RtlLocaleWithSuffix) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix)); - ASSERT_TRUE(ui_->GetRtlLocale()); + ASSERT_TRUE(ui_->rtl_locale_); } -TEST_F(ScreenRecoveryUITest, ShowMenu) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -375,7 +443,7 @@ TEST_F(ScreenRecoveryUITest, ShowMenu) { std::placeholders::_1, std::placeholders::_2))); } -TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -388,7 +456,7 @@ TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) { std::placeholders::_1, std::placeholders::_2))); } -TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -399,7 +467,7 @@ TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) { ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr)); } -TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -417,7 +485,7 @@ TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) { std::placeholders::_1, std::placeholders::_2))); } -TEST_F(ScreenRecoveryUITest, ShowMenuWithInterrupt) { +TEST_F(DISABLED_ScreenRecoveryUITest, ShowMenuWithInterrupt) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -449,7 +517,7 @@ TEST_F(ScreenRecoveryUITest, ShowMenuWithInterrupt) { std::placeholders::_1, std::placeholders::_2))); } -TEST_F(ScreenRecoveryUITest, LoadAnimation) { +TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -469,17 +537,17 @@ TEST_F(ScreenRecoveryUITest, LoadAnimation) { } Paths::Get().set_resource_dir(resource_dir.path); - ui_->RunLoadAnimation(); + ui_->LoadAnimation(); - ASSERT_EQ(2u, ui_->GetIntroFrames()); - ASSERT_EQ(3u, ui_->GetLoopFrames()); + ASSERT_EQ(2u, ui_->intro_frames_.size()); + ASSERT_EQ(3u, ui_->loop_frames_.size()); for (const auto& name : tempfiles) { ASSERT_EQ(0, unlink(name.c_str())); } } -TEST_F(ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { +TEST_F(DISABLED_ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { RETURN_IF_NO_GRAPHICS; ASSERT_TRUE(ui_->Init(kTestLocale)); @@ -488,7 +556,7 @@ TEST_F(ScreenRecoveryUITest, LoadAnimation_MissingAnimation) { Paths::Get().set_resource_dir("/proc/self"); ::testing::FLAGS_gtest_death_test_style = "threadsafe"; - ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), ""); + ASSERT_EXIT(ui_->LoadAnimation(), ::testing::KilledBySignal(SIGABRT), ""); } #undef RETURN_IF_NO_GRAPHICS diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp index de8ff7065..77625dbe9 100644 --- a/tests/unit/sysutil_test.cpp +++ b/tests/unit/sysutil_test.cpp @@ -17,7 +17,6 @@ #include <string> #include <android-base/file.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> #include "otautil/sysutil.h" diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp index 47f33d9ea..dfe617ebe 100644 --- a/tests/unit/zip_test.cpp +++ b/tests/unit/zip_test.cpp @@ -21,7 +21,6 @@ #include <vector> #include <android-base/file.h> -#include <android-base/test_utils.h> #include <gtest/gtest.h> #include <ziparchive/zip_archive.h> diff --git a/tools/dumpkey/DumpPublicKey.java b/tools/dumpkey/DumpPublicKey.java deleted file mode 100644 index 3eb139842..000000000 --- a/tools/dumpkey/DumpPublicKey.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright (C) 2008 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.dumpkey; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import java.io.FileInputStream; -import java.math.BigInteger; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; -import java.security.KeyStore; -import java.security.Key; -import java.security.PublicKey; -import java.security.Security; -import java.security.interfaces.ECPublicKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.ECPoint; - -/** - * Command line tool to extract RSA public keys from X.509 certificates - * and output source code with data initializers for the keys. - * @hide - */ -class DumpPublicKey { - /** - * @param key to perform sanity checks on - * @return version number of key. Supported versions are: - * 1: 2048-bit RSA key with e=3 and SHA-1 hash - * 2: 2048-bit RSA key with e=65537 and SHA-1 hash - * 3: 2048-bit RSA key with e=3 and SHA-256 hash - * 4: 2048-bit RSA key with e=65537 and SHA-256 hash - * @throws Exception if the key has the wrong size or public exponent - */ - static int checkRSA(RSAPublicKey key, boolean useSHA256) throws Exception { - BigInteger pubexp = key.getPublicExponent(); - BigInteger modulus = key.getModulus(); - int version; - - if (pubexp.equals(BigInteger.valueOf(3))) { - version = useSHA256 ? 3 : 1; - } else if (pubexp.equals(BigInteger.valueOf(65537))) { - version = useSHA256 ? 4 : 2; - } else { - throw new Exception("Public exponent should be 3 or 65537 but is " + - pubexp.toString(10) + "."); - } - - if (modulus.bitLength() != 2048) { - throw new Exception("Modulus should be 2048 bits long but is " + - modulus.bitLength() + " bits."); - } - - return version; - } - - /** - * @param key to perform sanity checks on - * @return version number of key. Supported versions are: - * 5: 256-bit EC key with curve NIST P-256 - * @throws Exception if the key has the wrong size or public exponent - */ - static int checkEC(ECPublicKey key) throws Exception { - if (key.getParams().getCurve().getField().getFieldSize() != 256) { - throw new Exception("Curve must be NIST P-256"); - } - - return 5; - } - - /** - * Perform sanity check on public key. - */ - static int check(PublicKey key, boolean useSHA256) throws Exception { - if (key instanceof RSAPublicKey) { - return checkRSA((RSAPublicKey) key, useSHA256); - } else if (key instanceof ECPublicKey) { - if (!useSHA256) { - throw new Exception("Must use SHA-256 with EC keys!"); - } - return checkEC((ECPublicKey) key); - } else { - throw new Exception("Unsupported key class: " + key.getClass().getName()); - } - } - - /** - * @param key to output - * @return a String representing this public key. If the key is a - * version 1 key, the string will be a C initializer; this is - * not true for newer key versions. - */ - static String printRSA(RSAPublicKey key, boolean useSHA256) throws Exception { - int version = check(key, useSHA256); - - BigInteger N = key.getModulus(); - - StringBuilder result = new StringBuilder(); - - int nwords = N.bitLength() / 32; // # of 32 bit integers in modulus - - if (version > 1) { - result.append("v"); - result.append(Integer.toString(version)); - result.append(" "); - } - - result.append("{"); - result.append(nwords); - - BigInteger B = BigInteger.valueOf(0x100000000L); // 2^32 - BigInteger N0inv = B.subtract(N.modInverse(B)); // -1 / N[0] mod 2^32 - - result.append(",0x"); - result.append(N0inv.toString(16)); - - BigInteger R = BigInteger.valueOf(2).pow(N.bitLength()); - BigInteger RR = R.multiply(R).mod(N); // 2^4096 mod N - - // Write out modulus as little endian array of integers. - result.append(",{"); - for (int i = 0; i < nwords; ++i) { - long n = N.mod(B).longValue(); - result.append(n); - - if (i != nwords - 1) { - result.append(","); - } - - N = N.divide(B); - } - result.append("}"); - - // Write R^2 as little endian array of integers. - result.append(",{"); - for (int i = 0; i < nwords; ++i) { - long rr = RR.mod(B).longValue(); - result.append(rr); - - if (i != nwords - 1) { - result.append(","); - } - - RR = RR.divide(B); - } - result.append("}"); - - result.append("}"); - return result.toString(); - } - - /** - * @param key to output - * @return a String representing this public key. If the key is a - * version 1 key, the string will be a C initializer; this is - * not true for newer key versions. - */ - static String printEC(ECPublicKey key) throws Exception { - int version = checkEC(key); - - StringBuilder result = new StringBuilder(); - - result.append("v"); - result.append(Integer.toString(version)); - result.append(" "); - - BigInteger X = key.getW().getAffineX(); - BigInteger Y = key.getW().getAffineY(); - int nbytes = key.getParams().getCurve().getField().getFieldSize() / 8; // # of 32 bit integers in X coordinate - - result.append("{"); - result.append(nbytes); - - BigInteger B = BigInteger.valueOf(0x100L); // 2^8 - - // Write out Y coordinate as array of characters. - result.append(",{"); - for (int i = 0; i < nbytes; ++i) { - long n = X.mod(B).longValue(); - result.append(n); - - if (i != nbytes - 1) { - result.append(","); - } - - X = X.divide(B); - } - result.append("}"); - - // Write out Y coordinate as array of characters. - result.append(",{"); - for (int i = 0; i < nbytes; ++i) { - long n = Y.mod(B).longValue(); - result.append(n); - - if (i != nbytes - 1) { - result.append(","); - } - - Y = Y.divide(B); - } - result.append("}"); - - result.append("}"); - return result.toString(); - } - - static String print(PublicKey key, boolean useSHA256) throws Exception { - if (key instanceof RSAPublicKey) { - return printRSA((RSAPublicKey) key, useSHA256); - } else if (key instanceof ECPublicKey) { - return printEC((ECPublicKey) key); - } else { - throw new Exception("Unsupported key class: " + key.getClass().getName()); - } - } - - public static void main(String[] args) { - if (args.length < 1) { - System.err.println("Usage: DumpPublicKey certfile ... > source.c"); - System.exit(1); - } - Security.addProvider(new BouncyCastleProvider()); - try { - for (int i = 0; i < args.length; i++) { - FileInputStream input = new FileInputStream(args[i]); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - X509Certificate cert = (X509Certificate) cf.generateCertificate(input); - - boolean useSHA256 = false; - String sigAlg = cert.getSigAlgName(); - if ("SHA1withRSA".equals(sigAlg) || "MD5withRSA".equals(sigAlg)) { - // SignApk has historically accepted "MD5withRSA" - // certificates, but treated them as "SHA1withRSA" - // anyway. Continue to do so for backwards - // compatibility. - useSHA256 = false; - } else if ("SHA256withRSA".equals(sigAlg) || "SHA256withECDSA".equals(sigAlg)) { - useSHA256 = true; - } else { - System.err.println(args[i] + ": unsupported signature algorithm \"" + - sigAlg + "\""); - System.exit(1); - } - - PublicKey key = cert.getPublicKey(); - check(key, useSHA256); - System.out.print(print(key, useSHA256)); - System.out.println(i < args.length - 1 ? "," : ""); - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(1); - } - System.exit(0); - } -} diff --git a/tools/dumpkey/DumpPublicKey.mf b/tools/dumpkey/DumpPublicKey.mf deleted file mode 100644 index 7bb3bc88d..000000000 --- a/tools/dumpkey/DumpPublicKey.mf +++ /dev/null @@ -1 +0,0 @@ -Main-Class: com.android.dumpkey.DumpPublicKey diff --git a/tools/dumpkey/Android.bp b/tools/image_generator/Android.bp index eb45e3176..2afdd5a84 100644 --- a/tools/dumpkey/Android.bp +++ b/tools/image_generator/Android.bp @@ -13,15 +13,16 @@ // limitations under the License. java_library_host { - name: "dumpkey", + name: "RecoveryImageGenerator", - manifest: "DumpPublicKey.mf", + manifest: "ImageGenerator.mf", - srcs: [ - "DumpPublicKey.java", + static_libs: [ + "commons-cli-1.2", + "icu4j-host", ], - static_libs: [ - "bouncycastle-host", + srcs: [ + "ImageGenerator.java", ], } diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java new file mode 100644 index 000000000..fd8e54295 --- /dev/null +++ b/tools/image_generator/ImageGenerator.java @@ -0,0 +1,757 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.recovery.tools; + +import com.ibm.icu.text.BreakIterator; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.OptionBuilder; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.font.TextAttribute; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.text.AttributedString; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.imageio.ImageIO; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +/** Command line tool to generate the localized image for recovery mode. */ +public class ImageGenerator { + // Initial height of the image to draw. + private static final int INITIAL_HEIGHT = 20000; + + private static final float DEFAULT_FONT_SIZE = 40; + + private static final Logger LOGGER = Logger.getLogger(ImageGenerator.class.getName()); + + // This is the canvas we used to draw texts. + private BufferedImage mBufferedImage; + + // The width in pixels of our image. The value will be adjusted once when we calculate the + // maximum width to fit the wrapped text strings. + private int mImageWidth; + + // The current height in pixels of our image. We will adjust the value when drawing more texts. + private int mImageHeight; + + // The current vertical offset in pixels to draw the top edge of new text strings. + private int mVerticalOffset; + + // The font size to draw the texts. + private final float mFontSize; + + // The name description of the text to localize. It's used to find the translated strings in the + // resource file. + private final String mTextName; + + // The directory that contains all the needed font files (e.g. ttf, otf, ttc files). + private final String mFontDirPath; + + // Align the text in the center of the image. + private final boolean mCenterAlignment; + + // Some localized font cannot draw the word "Android" and some PUNCTUATIONS; we need to fall + // back to use our default latin font instead. + private static final char[] PUNCTUATIONS = {',', ';', '.', '!', '?'}; + + private static final String ANDROID_STRING = "Android"; + + // The width of the word "Android" when drawing with the default font. + private int mAndroidStringWidth; + + // The default Font to draw latin characters. It's loaded from DEFAULT_FONT_NAME. + private Font mDefaultFont; + // Cache of the loaded fonts for all languages. + private Map<String, Font> mLoadedFontMap; + + // An explicit map from language to the font name to use. + // The map is extracted from frameworks/base/data/fonts/fonts.xml. + // And the language-subtag-registry is found in: + // https://www.iana.org/assignments/language-subtag-registry/language-subtag-registry + private static final String DEFAULT_FONT_NAME = "Roboto-Regular"; + private static final Map<String, String> LANGUAGE_TO_FONT_MAP = + new TreeMap<String, String>() { + { + put("am", "NotoSansEthiopic-Regular"); + put("ar", "NotoNaskhArabicUI-Regular"); + put("as", "NotoSansBengaliUI-Regular"); + put("bn", "NotoSansBengaliUI-Regular"); + put("fa", "NotoNaskhArabicUI-Regular"); + put("gu", "NotoSansGujaratiUI-Regular"); + put("hi", "NotoSansDevanagariUI-Regular"); + put("hy", "NotoSansArmenian-Regular"); + put("iw", "NotoSansHebrew-Regular"); + put("ja", "NotoSansCJK-Regular"); + put("ka", "NotoSansGeorgian-Regular"); + put("ko", "NotoSansCJK-Regular"); + put("km", "NotoSansKhmerUI-Regular"); + put("kn", "NotoSansKannadaUI-Regular"); + put("lo", "NotoSansLaoUI-Regular"); + put("ml", "NotoSansMalayalamUI-Regular"); + put("mr", "NotoSansDevanagariUI-Regular"); + put("my", "NotoSansMyanmarUI-Regular"); + put("ne", "NotoSansDevanagariUI-Regular"); + put("or", "NotoSansOriya-Regular"); + put("pa", "NotoSansGurmukhiUI-Regular"); + put("si", "NotoSansSinhala-Regular"); + put("ta", "NotoSansTamilUI-Regular"); + put("te", "NotoSansTeluguUI-Regular"); + put("th", "NotoSansThaiUI-Regular"); + put("ur", "NotoNaskhArabicUI-Regular"); + put("zh", "NotoSansCJK-Regular"); + } + }; + + // Languages that write from right to left. + private static final Set<String> RTL_LANGUAGE = + new HashSet<String>() { + { + add("ar"); // Arabic + add("fa"); // Persian + add("he"); // Hebrew + add("iw"); // Hebrew + add("ur"); // Urdu + } + }; + + /** Exception to indicate the failure to find the translated text strings. */ + public static class LocalizedStringNotFoundException extends Exception { + public LocalizedStringNotFoundException(String message) { + super(message); + } + + public LocalizedStringNotFoundException(String message, Throwable cause) { + super(message, cause); + } + } + + /** + * This class maintains the content of wrapped text, the attributes to draw these text, and + * the width of each wrapped lines. + */ + private class WrappedTextInfo { + /** LineInfo holds the AttributedString and width of each wrapped line. */ + private class LineInfo { + public AttributedString mLineContent; + public int mLineWidth; + + LineInfo(AttributedString text, int width) { + mLineContent = text; + mLineWidth = width; + } + } + + // Maintains the content of each line, as well as the width needed to draw these lines for + // a given language. + public List<LineInfo> mWrappedLines; + + WrappedTextInfo() { + mWrappedLines = new ArrayList<>(); + } + + /** + * Checks if the given text has words "Android" and some PUNCTUATIONS. If it does, and its + * associated textFont cannot display them correctly (e.g. for persian and hebrew); sets the + * attributes of these substrings to use our default font instead. + * + * @param text the input string to perform the check on + * @param width the pre-calculated width for the given text + * @param textFont the localized font to draw the input string + * @param fallbackFont our default font to draw latin characters + */ + public void addLine(String text, int width, Font textFont, Font fallbackFont) { + AttributedString attributedText = new AttributedString(text); + attributedText.addAttribute(TextAttribute.FONT, textFont); + attributedText.addAttribute(TextAttribute.SIZE, mFontSize); + + // Skips the check if we don't specify a fallbackFont. + if (fallbackFont != null) { + // Adds the attribute to use default font to draw the word "Android". + if (text.contains(ANDROID_STRING) + && textFont.canDisplayUpTo(ANDROID_STRING) != -1) { + int index = text.indexOf(ANDROID_STRING); + attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index, + index + ANDROID_STRING.length()); + } + + // Adds the attribute to use default font to draw the PUNCTUATIONS ", . ; ! ?" + for (char punctuation : PUNCTUATIONS) { + // TODO (xunchang) handle the RTL language that has different directions for '?' + if (text.indexOf(punctuation) != -1 && !textFont.canDisplay(punctuation)) { + int index = 0; + while ((index = text.indexOf(punctuation, index)) != -1) { + attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index, + index + 1); + index += 1; + } + } + } + } + + mWrappedLines.add(new LineInfo(attributedText, width)); + } + + /** Merges two WrappedTextInfo. */ + public void addLines(WrappedTextInfo other) { + mWrappedLines.addAll(other.mWrappedLines); + } + } + + /** Initailizes the fields of the image image. */ + public ImageGenerator( + int initialImageWidth, + String textName, + float fontSize, + String fontDirPath, + boolean centerAlignment) { + mImageWidth = initialImageWidth; + mImageHeight = INITIAL_HEIGHT; + mVerticalOffset = 0; + + // Initialize the canvas with the default height. + mBufferedImage = new BufferedImage(mImageWidth, mImageHeight, BufferedImage.TYPE_BYTE_GRAY); + + mTextName = textName; + mFontSize = fontSize; + mFontDirPath = fontDirPath; + mLoadedFontMap = new TreeMap<>(); + + mCenterAlignment = centerAlignment; + } + + /** + * Finds the translated text string for the given textName by parsing the resourceFile. Example + * of the xml fields: <resources xmlns:android="http://schemas.android.com/apk/res/android"> + * <string name="recovery_installing_security" msgid="9184031299717114342"> "Sicherheitsupdate + * wird installiert"</string> </resources> + * + * @param resourceFile the input resource file in xml format. + * @param textName the name description of the text. + * @return the string representation of the translated text. + */ + private String getTextString(File resourceFile, String textName) + throws IOException, ParserConfigurationException, org.xml.sax.SAXException, + LocalizedStringNotFoundException { + DocumentBuilderFactory builder = DocumentBuilderFactory.newInstance(); + DocumentBuilder db = builder.newDocumentBuilder(); + + Document doc = db.parse(resourceFile); + doc.getDocumentElement().normalize(); + + NodeList nodeList = doc.getElementsByTagName("string"); + for (int i = 0; i < nodeList.getLength(); i++) { + Node node = nodeList.item(i); + String name = node.getAttributes().getNamedItem("name").getNodeValue(); + if (name.equals(textName)) { + return node.getTextContent(); + } + } + + throw new LocalizedStringNotFoundException( + textName + " not found in " + resourceFile.getName()); + } + + /** Constructs the locale from the name of the resource file. */ + private Locale getLocaleFromFilename(String filename) throws IOException { + // Gets the locale string by trimming the top "values-". + String localeString = filename.substring(7); + if (localeString.matches("[A-Za-z]+")) { + return Locale.forLanguageTag(localeString); + } + if (localeString.matches("[A-Za-z]+-r[A-Za-z]+")) { + // "${Language}-r${Region}". e.g. en-rGB + String[] tokens = localeString.split("-r"); + return Locale.forLanguageTag(String.join("-", tokens)); + } + if (localeString.startsWith("b+")) { + // The special case of b+sr+Latn, which has the form "b+${Language}+${ScriptName}" + String[] tokens = localeString.substring(2).split("\\+"); + return Locale.forLanguageTag(String.join("-", tokens)); + } + + throw new IOException("Unrecognized locale string " + localeString); + } + + /** + * Iterates over the xml files in the format of values-$LOCALE/strings.xml under the resource + * directory and collect the translated text. + * + * @param resourcePath the path to the resource directory + * @param localesSet a list of supported locales; resources of other locales will be omitted. + * @return a map with the locale as key, and translated text as value + * @throws LocalizedStringNotFoundException if we cannot find the translated text for the given + * locale + */ + public Map<Locale, String> readLocalizedStringFromXmls(String resourcePath, + Set<String> localesSet) throws IOException, LocalizedStringNotFoundException { + File resourceDir = new File(resourcePath); + if (!resourceDir.isDirectory()) { + throw new LocalizedStringNotFoundException(resourcePath + " is not a directory."); + } + + Map<Locale, String> result = + // Overrides the string comparator so that sr is sorted behind sr-Latn. And thus + // recovery can find the most relevant locale when going down the list. + new TreeMap<>( + (Locale l1, Locale l2) -> { + if (l1.toLanguageTag().equals(l2.toLanguageTag())) { + return 0; + } + if (l1.getLanguage().equals(l2.toLanguageTag())) { + return -1; + } + if (l2.getLanguage().equals(l1.toLanguageTag())) { + return 1; + } + return l1.toLanguageTag().compareTo(l2.toLanguageTag()); + }); + + // Find all the localized resource subdirectories in the format of values-$LOCALE + String[] nameList = + resourceDir.list((File file, String name) -> name.startsWith("values-")); + for (String name : nameList) { + String localeString = name.substring(7); + if (localesSet != null && !localesSet.contains(localeString)) { + LOGGER.info("Skip parsing text for locale " + localeString); + continue; + } + + File textFile = new File(resourcePath, name + "/strings.xml"); + String localizedText; + try { + localizedText = getTextString(textFile, mTextName); + } catch (IOException | ParserConfigurationException | org.xml.sax.SAXException e) { + throw new LocalizedStringNotFoundException( + "Failed to read the translated text for locale " + name, e); + } + + Locale locale = getLocaleFromFilename(name); + // Removes the double quotation mark from the text. + result.put(locale, localizedText.substring(1, localizedText.length() - 1)); + } + + return result; + } + + /** + * Returns a font object associated given the given locale + * + * @throws IOException if the font file fails to open + * @throws FontFormatException if the font file doesn't have the expected format + */ + private Font loadFontsByLocale(String language) throws IOException, FontFormatException { + if (mLoadedFontMap.containsKey(language)) { + return mLoadedFontMap.get(language); + } + + String fontName = LANGUAGE_TO_FONT_MAP.getOrDefault(language, DEFAULT_FONT_NAME); + String[] suffixes = {".otf", ".ttf", ".ttc"}; + for (String suffix : suffixes) { + File fontFile = new File(mFontDirPath, fontName + suffix); + if (fontFile.isFile()) { + Font result = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize); + mLoadedFontMap.put(language, result); + return result; + } + } + + throw new IOException( + "Can not find the font file " + fontName + " for language " + language); + } + + /** Wraps the text with a maximum of mImageWidth pixels per line. */ + private WrappedTextInfo wrapText(String text, FontMetrics metrics) { + WrappedTextInfo info = new WrappedTextInfo(); + + BreakIterator lineBoundary = BreakIterator.getLineInstance(); + lineBoundary.setText(text); + + int lineWidth = 0; // Width of the processed words of the current line. + int start = lineBoundary.first(); + StringBuilder line = new StringBuilder(); + for (int end = lineBoundary.next(); end != BreakIterator.DONE; + start = end, end = lineBoundary.next()) { + String token = text.substring(start, end); + int tokenWidth = metrics.stringWidth(token); + // Handles the width mismatch of the word "Android" between different fonts. + if (token.contains(ANDROID_STRING) + && metrics.getFont().canDisplayUpTo(ANDROID_STRING) != -1) { + tokenWidth = tokenWidth - metrics.stringWidth(ANDROID_STRING) + mAndroidStringWidth; + } + + if (lineWidth + tokenWidth > mImageWidth) { + info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont); + + line = new StringBuilder(); + lineWidth = 0; + } + line.append(token); + lineWidth += tokenWidth; + } + + info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont); + + return info; + } + + /** + * Handles the special characters of the raw text embedded in the xml file; and wraps the text + * with a maximum of mImageWidth pixels per line. + * + * @param text the string representation of text to wrap + * @param metrics the metrics of the Font used to draw the text; it gives the width in pixels of + * the text given its string representation + * @return a WrappedTextInfo class with the width of each AttributedString smaller than + * mImageWidth pixels + */ + private WrappedTextInfo processAndWrapText(String text, FontMetrics metrics) { + // Apostrophe is escaped in the xml file. + String processed = text.replace("\\'", "'"); + // The separator "\n\n" indicates a new line in the text. + String[] lines = processed.split("\\\\n\\\\n"); + WrappedTextInfo result = new WrappedTextInfo(); + for (String line : lines) { + result.addLines(wrapText(line, metrics)); + } + + return result; + } + + /** + * Encodes the information of the text image for |locale|. According to minui/resources.cpp, the + * width, height and locale of the image is decoded as: int w = (row[1] << 8) | row[0]; int h = + * (row[3] << 8) | row[2]; __unused int len = row[4]; char* loc = + * reinterpret_cast<char*>(&row[5]); + */ + private List<Integer> encodeTextInfo(int width, int height, String locale) { + List<Integer> info = + new ArrayList<>( + Arrays.asList( + width & 0xff, + width >> 8, + height & 0xff, + height >> 8, + locale.length())); + + byte[] localeBytes = locale.getBytes(); + for (byte b : localeBytes) { + info.add((int) b); + } + info.add(0); + + return info; + } + + /** Returns Graphics2D object that uses the given locale. */ + private Graphics2D createGraphics(Locale locale) throws IOException, FontFormatException { + Graphics2D graphics = mBufferedImage.createGraphics(); + graphics.setColor(Color.WHITE); + graphics.setRenderingHint( + RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP); + graphics.setFont(loadFontsByLocale(locale.getLanguage())); + + return graphics; + } + + /** Returns the maximum screen width needed to fit the given text after wrapping. */ + private int measureTextWidth(String text, Locale locale) + throws IOException, FontFormatException { + Graphics2D graphics = createGraphics(locale); + FontMetrics fontMetrics = graphics.getFontMetrics(); + WrappedTextInfo wrappedTextInfo = processAndWrapText(text, fontMetrics); + + int textWidth = 0; + for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) { + textWidth = Math.max(textWidth, lineInfo.mLineWidth); + } + + // This may happen if one single word is larger than the image width. + if (textWidth > mImageWidth) { + throw new IllegalStateException( + "Wrapped text width " + + textWidth + + " is larger than image width " + + mImageWidth + + " for locale: " + + locale); + } + + return textWidth; + } + + /** + * Draws the text string on the canvas for given locale. + * + * @param text the string to draw on canvas + * @param locale the current locale tag of the string to draw + * @throws IOException if we cannot find the corresponding font file for the given locale. + * @throws FontFormatException if we failed to load the font file for the given locale. + */ + private void drawText(String text, Locale locale, String languageTag) + throws IOException, FontFormatException { + LOGGER.info("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text); + + Graphics2D graphics = createGraphics(locale); + FontMetrics fontMetrics = graphics.getFontMetrics(); + WrappedTextInfo wrappedTextInfo = processAndWrapText(text, fontMetrics); + + // Marks the start y offset for the text image of current locale; and reserves one line to + // encode the image metadata. + int currentImageStart = mVerticalOffset; + mVerticalOffset += 1; + for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) { + int lineHeight = fontMetrics.getHeight(); + // Doubles the height of the image if we are short of space. + if (mVerticalOffset + lineHeight >= mImageHeight) { + resize(mImageWidth, mImageHeight * 2); + // Recreates the graphics since it's attached to the buffered image. + graphics = createGraphics(locale); + } + + // Draws the text at mVerticalOffset and increments the offset with line space. + int baseLine = mVerticalOffset + lineHeight - fontMetrics.getDescent(); + + // Draws from right if it's an RTL language. + int x = + mCenterAlignment + ? (mImageWidth - lineInfo.mLineWidth) / 2 + : RTL_LANGUAGE.contains(languageTag) + ? mImageWidth - lineInfo.mLineWidth + : 0; + graphics.drawString(lineInfo.mLineContent.getIterator(), x, baseLine); + + mVerticalOffset += lineHeight; + } + + // Encodes the metadata of the current localized image as pixels. + int currentImageHeight = mVerticalOffset - currentImageStart - 1; + List<Integer> info = encodeTextInfo(mImageWidth, currentImageHeight, languageTag); + for (int i = 0; i < info.size(); i++) { + int[] pixel = {info.get(i)}; + mBufferedImage.getRaster().setPixel(i, currentImageStart, pixel); + } + } + + /** + * Redraws the image with the new width and new height. + * + * @param width the new width of the image in pixels. + * @param height the new height of the image in pixels. + */ + private void resize(int width, int height) { + BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY); + Graphics2D graphic = resizedImage.createGraphics(); + graphic.drawImage(mBufferedImage, 0, 0, null); + graphic.dispose(); + + mBufferedImage = resizedImage; + mImageWidth = width; + mImageHeight = height; + } + + /** + * This function draws the font characters and saves the result to outputPath. + * + * @param localizedTextMap a map from locale to its translated text string + * @param outputPath the path to write the generated image file. + * @throws FontFormatException if there's a format error in one of the font file + * @throws IOException if we cannot find the font file for one of the locale, or we failed to + * write the image file. + */ + public void generateImage(Map<Locale, String> localizedTextMap, String outputPath) + throws FontFormatException, IOException { + FontMetrics defaultFontMetrics = + createGraphics(Locale.forLanguageTag("en")).getFontMetrics(); + mDefaultFont = defaultFontMetrics.getFont(); + mAndroidStringWidth = defaultFontMetrics.stringWidth(ANDROID_STRING); + + // The last country variant should be the fallback locale for a given language. + Map<String, Locale> fallbackLocaleMap = new HashMap<>(); + int textWidth = 0; + for (Locale locale : localizedTextMap.keySet()) { + // Updates the fallback locale if we have a new language variant. Don't do it for en-XC + // as it's a pseudo-locale. + if (!locale.toLanguageTag().equals("en-XC")) { + fallbackLocaleMap.put(locale.getLanguage(), locale); + } + textWidth = Math.max(textWidth, measureTextWidth(localizedTextMap.get(locale), locale)); + } + + // Removes the black margins to reduce the size of the image. + resize(textWidth, mImageHeight); + + for (Locale locale : localizedTextMap.keySet()) { + // Recovery expects en-US instead of en_US. + String languageTag = locale.toLanguageTag(); + Locale fallbackLocale = fallbackLocaleMap.get(locale.getLanguage()); + if (locale.equals(fallbackLocale)) { + // Makes the last country variant for a given language be the catch-all for that + // language. + languageTag = locale.getLanguage(); + } else if (localizedTextMap.get(locale).equals(localizedTextMap.get(fallbackLocale))) { + LOGGER.info("Skip parsing text for duplicate locale " + locale); + continue; + } + + drawText(localizedTextMap.get(locale), locale, languageTag); + } + + resize(mImageWidth, mVerticalOffset); + ImageIO.write(mBufferedImage, "png", new File(outputPath)); + } + + /** Prints the helper message. */ + public static void printUsage(Options options) { + new HelpFormatter().printHelp("java -jar path_to_jar [required_options]", options); + } + + /** Creates the command line options. */ + public static Options createOptions() { + Options options = new Options(); + options.addOption( + OptionBuilder.withLongOpt("image_width") + .withDescription("The initial width of the image in pixels.") + .hasArgs(1) + .isRequired() + .create()); + + options.addOption( + OptionBuilder.withLongOpt("text_name") + .withDescription( + "The description of the text string, e.g. recovery_erasing") + .hasArgs(1) + .isRequired() + .create()); + + options.addOption( + OptionBuilder.withLongOpt("font_dir") + .withDescription( + "The directory that contains all the support font format files, " + + "e.g. $OUT/system/fonts/") + .hasArgs(1) + .isRequired() + .create()); + + options.addOption( + OptionBuilder.withLongOpt("resource_dir") + .withDescription( + "The resource directory that contains all the translated strings in" + + " xml format, e.g." + + " bootable/recovery/tools/recovery_l10n/res/") + .hasArgs(1) + .isRequired() + .create()); + + options.addOption( + OptionBuilder.withLongOpt("output_file") + .withDescription("Path to the generated image.") + .hasArgs(1) + .isRequired() + .create()); + + options.addOption( + OptionBuilder.withLongOpt("center_alignment") + .withDescription("Align the text in the center of the screen.") + .hasArg(false) + .create()); + + options.addOption( + OptionBuilder.withLongOpt("verbose") + .withDescription("Output the logging above info level.") + .hasArg(false) + .create()); + + options.addOption( + OptionBuilder.withLongOpt("locales") + .withDescription("A list of android locales separated by ',' e.g." + + " 'af,en,zh-rTW'") + .hasArg(true) + .create()); + + return options; + } + + /** The main function parses the command line options and generates the desired text image. */ + public static void main(String[] args) + throws NumberFormatException, IOException, FontFormatException, + LocalizedStringNotFoundException { + Options options = createOptions(); + CommandLine cmd; + try { + cmd = new GnuParser().parse(options, args); + } catch (ParseException e) { + System.err.println(e.getMessage()); + printUsage(options); + return; + } + + int imageWidth = Integer.parseUnsignedInt(cmd.getOptionValue("image_width")); + + if (cmd.hasOption("verbose")) { + LOGGER.setLevel(Level.INFO); + } else { + LOGGER.setLevel(Level.WARNING); + } + + ImageGenerator imageGenerator = + new ImageGenerator( + imageWidth, + cmd.getOptionValue("text_name"), + DEFAULT_FONT_SIZE, + cmd.getOptionValue("font_dir"), + cmd.hasOption("center_alignment")); + + Set<String> localesSet = null; + if (cmd.hasOption("locales")) { + String[] localesList = cmd.getOptionValue("locales").split(","); + localesSet = new HashSet<>(Arrays.asList(localesList)); + // Ensures that we have the default locale, all english translations are identical. + localesSet.add("en-rAU"); + } + Map<Locale, String> localizedStringMap = + imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"), + localesSet); + imageGenerator.generateImage(localizedStringMap, cmd.getOptionValue("output_file")); + } +} diff --git a/tools/image_generator/ImageGenerator.mf b/tools/image_generator/ImageGenerator.mf new file mode 100644 index 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..5d70354e4 --- /dev/null +++ b/tools/image_generator/README.md @@ -0,0 +1,21 @@ +Recovery Image Generator +------------------------- + +This program uses java.awt.Graphics2D to generate the background text files used +under recovery mode. And thus we don't need to do the manual work by running +emulators with different dpi. + +# Usage: + `java -jar path_to_jar --image_width imageWidth --text_name textName --font_dir fontDirectory + --resource_dir resourceDirectory --output_file outputFilename` + +# Description of the parameters: +1. `imageWidth`: The number of pixels per line; and the text strings will be + wrapped accordingly. +2. `textName`: The description of the text string, e.g. "recovery_erasing", + "recovery_installing_security" +3. `fontDirectory`: The directory that contains all the support .ttf | .ttc + files, e.g. $OUT/system/fonts/ +4. `resourceDirectory`: The resource directory that contains all the translated + strings in xml format, e.g. bootable/recovery/tools/recovery_l10n/res/ +5. `outputFilename`: Path to the generated image. diff --git a/tools/recovery_l10n/res/values-af/strings.xml b/tools/recovery_l10n/res/values-af/strings.xml index b1974da20..85a3c9037 100644 --- a/tools/recovery_l10n/res/values-af/strings.xml +++ b/tools/recovery_l10n/res/values-af/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Geen opdrag nie"</string> <string name="recovery_error" msgid="5748178989622716736">"Fout!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installeer tans sekuriteitopdatering"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan nie Android-stelsel laai nie. Jou data is dalk korrup. As jy aanhou om hierdie boodskap te kry, sal jy dalk \'n fabrieksterugstelling moet doen en alle gebruikerdata moet uitvee wat op hierdie toestel geberg word."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Probeer weer"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Fabrieksterugstelling"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vee alle gebruikerdata uit?\n\n DIT KAN NIE ONTDOEN WORD NIE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Kanselleer"</string> </resources> diff --git a/tools/recovery_l10n/res/values-am/strings.xml b/tools/recovery_l10n/res/values-am/strings.xml index 75c17fbad..353f2233b 100644 --- a/tools/recovery_l10n/res/values-am/strings.xml +++ b/tools/recovery_l10n/res/values-am/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ምንም ትዕዛዝ የለም"</string> <string name="recovery_error" msgid="5748178989622716736">"ስህተት!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"የደህንነት ዝማኔ በመጫን ላይ"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"የAndroid ስርዓትን መጫን አልተቻለም። የእርስዎ ውሂብ የተበላሸ ሊሆን ይችላል። ይህን መልዕክት ማግኘቱን ከቀጠሉ የፋብሪካ ውሂብ ዳግም ማስጀመር ማከናወንና በዚህ መሣሪያ ላይ የተከማቸ ሁሉንም የተጠቃሚ ውሂብ መሰረዝ ሊኖርብዎት ይችላል።"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"እንደገና ሞክር"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"የፋብሪካ ውሂብ ዳግም ማስጀመር"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ሁሉም የተጠቃሚ ውሂብ ይሰረዝ?\n\n ይህ ሊቀለበስ አይችልም!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ይቅር"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ar/strings.xml b/tools/recovery_l10n/res/values-ar/strings.xml index 601b5832b..2af36d64a 100644 --- a/tools/recovery_l10n/res/values-ar/strings.xml +++ b/tools/recovery_l10n/res/values-ar/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ليس هناك أي أمر"</string> <string name="recovery_error" msgid="5748178989622716736">"خطأ!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"جارٍ تثبيت تحديث الأمان"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"يتعذَّر تحميل نظام Android، حيث قد تكون بياناتك تالفة. وإذا استمر ظهور هذه الرسالة، قد يتعيَّن عليك إجراء إعادة الضبط بحسب بيانات المصنع ومحو جميع بيانات المستخدم المُخزَّنة على هذا الجهاز."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"إعادة المحاولة"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"إعادة الضبط بحسب بيانات المصنع"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"هل تريد حجب كل بيانات المستخدم؟\n\n لا يمكن التراجع عن هذا الإجراء."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"إلغاء"</string> </resources> diff --git a/tools/recovery_l10n/res/values-as/strings.xml b/tools/recovery_l10n/res/values-as/strings.xml index 2624cebe4..33a204d05 100644 --- a/tools/recovery_l10n/res/values-as/strings.xml +++ b/tools/recovery_l10n/res/values-as/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"কোনো আদেশ নাই"</string> <string name="recovery_error" msgid="5748178989622716736">"ত্ৰুটি!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"সুৰক্ষা আপডেইট ইনষ্টল কৰি থকা হৈছে"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ছিষ্টেম ল\'ড কৰিব নোৱাৰি। আপোনাৰ ডেটাত কিবা আসোঁৱাহ থকা যেন লাগিছে। আপুনি যদি এই বাৰ্তাটো পায়েই থাকে, আপুনি নিজৰ ডিভাইচটো ফেক্টৰী ডেটা ৰিছেট কৰি সেইটোত থকা ব্যৱহাৰকাৰীৰ সকলো ডেটা মচিব লগা হ\'ব পাৰে।"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"আকৌ চেষ্টা কৰক"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ফেক্টৰী ডেটা ৰিছেট"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ব্যৱহাৰকাৰীৰ সকলো ডেটা মচিবনে?\n\n এইটো কৰাৰ পিছত আনডু কৰিব নোৱাৰি!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"বাতিল কৰক"</string> </resources> diff --git a/tools/recovery_l10n/res/values-az/strings.xml b/tools/recovery_l10n/res/values-az/strings.xml index c6765a9ea..35194c4b2 100644 --- a/tools/recovery_l10n/res/values-az/strings.xml +++ b/tools/recovery_l10n/res/values-az/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Əmr yoxdur"</string> <string name="recovery_error" msgid="5748178989622716736">"Xəta!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Təhlükəsizlik güncəlləməsi yüklənir"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android sistemi yüklənmir. Datanız zədələnə bilər. Bu mesajı yenə qəbul etsəniz, data zavod sıfırlamasını həyata keçirməli və bu cihazda saxlanmış istifadəçi datasının hamısını silməlisiniz."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Yenidən cəhd edin"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Data zavod sıfırlaması"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Bütün istifadəçi datası silinsin?\n\n BU ƏMƏLİYYATI GERİ QAYTARMAQ MÜMKÜN DEYİL!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ləğv edin"</string> </resources> diff --git a/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml b/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml index c2d8f2239..19c6f4194 100644 --- a/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml +++ b/tools/recovery_l10n/res/values-b+sr+Latn/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nema komande"</string> <string name="recovery_error" msgid="5748178989622716736">"Greška!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalira se bezbednosno ažuriranje"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Učitavanje Android sistema nije uspelo. Podaci su možda oštećeni. Ako nastavite da dobijate ovu poruku, možda ćete morati da resetujete uređaj na fabrička podešavanja i obrišete sve podatke korisnika koje čuvate na njemu."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Probaj ponovo"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Resetovanje na fabrička podešavanja"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite li da izbrišete sve podatke korisnika?\n\n OVO NE MOŽE DA SE OPOZOVE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Otkaži"</string> </resources> diff --git a/tools/recovery_l10n/res/values-be/strings.xml b/tools/recovery_l10n/res/values-be/strings.xml index 7c0954d31..ad14fbe27 100644 --- a/tools/recovery_l10n/res/values-be/strings.xml +++ b/tools/recovery_l10n/res/values-be/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Няма каманды"</string> <string name="recovery_error" msgid="5748178989622716736">"Памылка"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Усталёўка абнаўлення сістэмы бяспекі"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не ўдалося загрузіць сістэму Android. Магчыма, вашы даныя пашкоджаны. Калі вы зноў убачыце гэта паведамленне, скіньце налады прылады да заводскіх значэнняў і сатрыце ўсе карыстальніцкія даныя, якія на ёй захоўваюцца."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Паўтарыць спробу"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Скінуць да заводскіх налад"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ачысціць усе карыстальніцкія даныя?\n\n ГЭТА ДЗЕЯННЕ НЕЛЬГА АДРАБІЦЬ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Скасаваць"</string> </resources> diff --git a/tools/recovery_l10n/res/values-bg/strings.xml b/tools/recovery_l10n/res/values-bg/strings.xml index 9e628a2af..e96ff4464 100644 --- a/tools/recovery_l10n/res/values-bg/strings.xml +++ b/tools/recovery_l10n/res/values-bg/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Без команда"</string> <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Актуализацията на сигурносттa се инсталира"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Системата Android не може да се зареди. Данните ви може да са повредени. Ако продължите да получавате това съобщение, може да е необходимо да възстановите фабричните настройки и да изтриете всички потребителски данни, съхранени на това устройство."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Нов опит"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Възстановяване на фабричните настройки"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Да се изчистят ли всички потребителски данни?\n\n ТОВА ДЕЙСТВИЕ НЕ МОЖЕ ДА БЪДЕ ОТМЕНЕНО!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Отказ"</string> </resources> diff --git a/tools/recovery_l10n/res/values-bn/strings.xml b/tools/recovery_l10n/res/values-bn/strings.xml index 0a481faf1..5967bc4b8 100644 --- a/tools/recovery_l10n/res/values-bn/strings.xml +++ b/tools/recovery_l10n/res/values-bn/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"কোনো আদেশ নেই"</string> <string name="recovery_error" msgid="5748178989622716736">"ত্রুটি!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"নিরাপত্তার আপডেট ইনস্টল করা হচ্ছে"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android সিস্টেম লোড করা যায়নি। আপনার ডেটা হয়ত নষ্ট হয়ে গেছে। যদি এই মেসেজটি আসতেই থাকে তাহলে হয়ত ফ্যাক্টরি ডেটা রিসেট করে এই ডিভাইসে থাকা ব্যবহারকারীর সব ডেটা মুছে ফেলতে হবে।"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"আবার চেষ্টা করুন"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ফ্যাক্টরি ডেটা রিসেট করুন"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ব্যবহারকারীর সব ডেটা মুছে দিতে চান?\n\n এই ডেটা আর ফিরে পাওয়া যাবে না!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"বাতিল করুন"</string> </resources> diff --git a/tools/recovery_l10n/res/values-bs/strings.xml b/tools/recovery_l10n/res/values-bs/strings.xml index 412cf0276..38f197f29 100644 --- a/tools/recovery_l10n/res/values-bs/strings.xml +++ b/tools/recovery_l10n/res/values-bs/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nema komande"</string> <string name="recovery_error" msgid="5748178989622716736">"Greška!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instaliranje sigurnosnog ažuriranja…"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nije moguće učitati Android sistem. Podaci su možda oštećeni. Ako opet primite ovu poruku, možda ćete morati vratiti uređaj na fabričke postavke i izbrisati sve podatke korisnika pohranjene na ovom uređaju."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Pokušaj ponovo"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Vraćanje na fabričke postavke"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Izbrisati sve podatke korisnika?\n\n TA RADNJA SE NE MOŽE PONIŠTITI!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Otkaži"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ca/strings.xml b/tools/recovery_l10n/res/values-ca/strings.xml index 3f266d2df..6b7bec077 100644 --- a/tools/recovery_l10n/res/values-ca/strings.xml +++ b/tools/recovery_l10n/res/values-ca/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No hi ha cap ordre"</string> <string name="recovery_error" msgid="5748178989622716736">"S\'ha produït un error"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"S\'està instal·lant una actualització de seguretat"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No s\'ha pogut carregar el sistema Android. És possible que les teves dades estiguin malmeses. Si continues veient aquest missatge, pot ser que hagis de restablir les dades de fàbrica i esborrar totes les dades d\'usuari emmagatzemades en aquest dispositiu."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Torna-ho a provar"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restableix les dades de fàbrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vols eliminar totes les dades d\'usuari?\n\n AQUESTA ACCIÓ NO ES POT DESFER."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel·la"</string> </resources> diff --git a/tools/recovery_l10n/res/values-cs/strings.xml b/tools/recovery_l10n/res/values-cs/strings.xml index eb436a810..c42dab2c4 100644 --- a/tools/recovery_l10n/res/values-cs/strings.xml +++ b/tools/recovery_l10n/res/values-cs/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Žádný příkaz"</string> <string name="recovery_error" msgid="5748178989622716736">"Chyba!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalace aktualizace zabezpečení"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Systém Android se nepodařilo načíst. Vaše data jsou možná poškozena. Pokud se tato zpráva bude zobrazovat i nadále, bude nutné vymazat všechna uživatelská data v zařízení a obnovit tovární data."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Zkusit znovu"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Obnovení továrních dat"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vymazat všechna uživatelská data?\n\nTUTO AKCI NELZE VRÁTIT ZPĚT!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Zrušit"</string> </resources> diff --git a/tools/recovery_l10n/res/values-da/strings.xml b/tools/recovery_l10n/res/values-da/strings.xml index c6e64a245..814c0df09 100644 --- a/tools/recovery_l10n/res/values-da/strings.xml +++ b/tools/recovery_l10n/res/values-da/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ingen kommando"</string> <string name="recovery_error" msgid="5748178989622716736">"Fejl!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installerer sikkerhedsopdateringen"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-systemet kan ikke indlæses. Dine data er muligvis beskadigede. Hvis du bliver ved med at få denne meddelelse, er du måske nødt til at udføre en gendannelse af fabriksdata og slette alle brugerdata, der er gemt på denne enhed."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Prøv igen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gendannelse af fabriksdata"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vil du rydde alle brugerdata?\n\n DETTE KAN IKKE FORTRYDES!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuller"</string> </resources> diff --git a/tools/recovery_l10n/res/values-de/strings.xml b/tools/recovery_l10n/res/values-de/strings.xml index 6b6726a23..80fa97110 100644 --- a/tools/recovery_l10n/res/values-de/strings.xml +++ b/tools/recovery_l10n/res/values-de/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Kein Befehl"</string> <string name="recovery_error" msgid="5748178989622716736">"Fehler"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Sicherheitsupdate wird installiert"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-System kann nicht geladen werden. Deine Daten sind eventuell beschädigt. Wenn du diese Nachricht weiterhin erhältst, musst du dein Gerät unter Umständen auf die Werkseinstellungen zurücksetzen und alle darauf gespeicherten Nutzerdaten löschen."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Noch einmal versuchen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Zurücksetzen auf Werkseinstellungen"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Alle Nutzerdaten löschen?\n\n DIESE AKTION KANN NICHT RÜCKGÄNGIG GEMACHT WERDEN."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Abbrechen"</string> </resources> diff --git a/tools/recovery_l10n/res/values-el/strings.xml b/tools/recovery_l10n/res/values-el/strings.xml index 4cb2da5f9..204ae4092 100644 --- a/tools/recovery_l10n/res/values-el/strings.xml +++ b/tools/recovery_l10n/res/values-el/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Καμία εντολή"</string> <string name="recovery_error" msgid="5748178989622716736">"Σφάλμα!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Εγκατάσταση ενημέρωσης ασφαλείας"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Δεν είναι δυνατή η φόρτωση του συστήματος Android. Τα δεδομένα σας μπορεί να είναι κατεστραμμένα. Εάν εξακολουθήσετε να λαμβάνετε αυτό το μήνυμα, μπορεί να χρειαστεί να κάνετε επαναφορά εργοστασιακών ρυθμίσεων και να διαγράψετε όλα τα δεδομένα που έχουν αποθηκευτεί σε αυτήν τη συσκευή."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Δοκιμάστε ξανά"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Επαναφορά εργοστασιακών δεδομένων"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Να διαγραφούν όλα τα δεδομένα χρήστη;\n\n ΔΕΝ ΕΙΝΑΙ ΔΥΝΑΤΗ Η ΑΝΑΙΡΕΣΗ ΑΥΤΗΣ ΤΗΣ ΕΝΕΡΓΕΙΑΣ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ακύρωση"</string> </resources> diff --git a/tools/recovery_l10n/res/values-en-rAU/strings.xml b/tools/recovery_l10n/res/values-en-rAU/strings.xml index dc75c2374..6451e5b6c 100644 --- a/tools/recovery_l10n/res/values-en-rAU/strings.xml +++ b/tools/recovery_l10n/res/values-en-rAU/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot load Android system. Your data may be corrupt. If you continue to get this message, you may need to perform a factory data reset and erase all user data stored on this device."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string> </resources> diff --git a/tools/recovery_l10n/res/values-en-rCA/strings.xml b/tools/recovery_l10n/res/values-en-rCA/strings.xml index dc75c2374..6451e5b6c 100644 --- a/tools/recovery_l10n/res/values-en-rCA/strings.xml +++ b/tools/recovery_l10n/res/values-en-rCA/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot load Android system. Your data may be corrupt. If you continue to get this message, you may need to perform a factory data reset and erase all user data stored on this device."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string> </resources> diff --git a/tools/recovery_l10n/res/values-en-rGB/strings.xml b/tools/recovery_l10n/res/values-en-rGB/strings.xml index dc75c2374..6451e5b6c 100644 --- a/tools/recovery_l10n/res/values-en-rGB/strings.xml +++ b/tools/recovery_l10n/res/values-en-rGB/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot load Android system. Your data may be corrupt. If you continue to get this message, you may need to perform a factory data reset and erase all user data stored on this device."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string> </resources> diff --git a/tools/recovery_l10n/res/values-en-rIN/strings.xml b/tools/recovery_l10n/res/values-en-rIN/strings.xml index dc75c2374..6451e5b6c 100644 --- a/tools/recovery_l10n/res/values-en-rIN/strings.xml +++ b/tools/recovery_l10n/res/values-en-rIN/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot load Android system. Your data may be corrupt. If you continue to get this message, you may need to perform a factory data reset and erase all user data stored on this device."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CANNOT BE UNDONE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string> </resources> diff --git a/tools/recovery_l10n/res/values-en-rXC/strings.xml b/tools/recovery_l10n/res/values-en-rXC/strings.xml index 2d528b3fb..61390f113 100644 --- a/tools/recovery_l10n/res/values-en-rXC/strings.xml +++ b/tools/recovery_l10n/res/values-en-rXC/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"No command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installing security update"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Cannot load Android system. Your data may be corrupt. If you continue to get this message, you may need to perform a factory data reset and erase all user data stored on this device."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Try again"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Factory data reset"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe all user data?\n\n THIS CAN NOT BE UNDONE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancel"</string> </resources> diff --git a/tools/recovery_l10n/res/values-es-rUS/strings.xml b/tools/recovery_l10n/res/values-es-rUS/strings.xml index 06b86069b..c0baa5924 100644 --- a/tools/recovery_l10n/res/values-es-rUS/strings.xml +++ b/tools/recovery_l10n/res/values-es-rUS/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ningún comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Error"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguridad"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No se puede cargar el sistema Android. Es posible que los datos estén dañados. Si este mensaje no desaparece, es posible que debas restablecer la configuración de fábrica del dispositivo y borrar todos los datos del usuario almacenados en él."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Reintentar"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecer configuración de fábrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"¿Quieres borrar todos los datos del usuario?\n\n ESTA ACCIÓN NO SE PUEDE DESHACER"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-es/strings.xml b/tools/recovery_l10n/res/values-es/strings.xml index d8618f2f4..de3b69bf3 100644 --- a/tools/recovery_l10n/res/values-es/strings.xml +++ b/tools/recovery_l10n/res/values-es/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Sin comandos"</string> <string name="recovery_error" msgid="5748178989622716736">"Error"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguridad"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"No se puede cargar el sistema Android. Es posible que tus datos estén dañados. Si sigue apareciendo este mensaje, es posible que tengas que restablecer el estado de fábrica y borrar todos los datos de usuario almacenados en este dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Reintentar"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecer estado de fábrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"¿Quieres borrar todos los datos de usuario?\n\n ESTA ACCIÓN NO SE PUEDE DESHACER."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-et/strings.xml b/tools/recovery_l10n/res/values-et/strings.xml index 072a9ef80..cafb32ffd 100644 --- a/tools/recovery_l10n/res/values-et/strings.xml +++ b/tools/recovery_l10n/res/values-et/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Käsk puudub"</string> <string name="recovery_error" msgid="5748178989622716736">"Viga!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Turvavärskenduse installimine"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-süsteemi ei saa laadida. Teie andmed on võib-olla rikutud. Kui jätkate selle sõnumi hankimist, peate võib-olla tegema tehaseandmetele lähtestamise ja kustutama kõik sellesse seadmesse salvestatud kasutajaandmed."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Proovige uuesti"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tehaseandmetele lähtestamine"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Kas kustutada kõik kasutajaandmed?\n\n SEDA TOIMINGUT EI SAA TAGASI VÕTTA!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Tühista"</string> </resources> diff --git a/tools/recovery_l10n/res/values-eu/strings.xml b/tools/recovery_l10n/res/values-eu/strings.xml index 5540469d0..005a04264 100644 --- a/tools/recovery_l10n/res/values-eu/strings.xml +++ b/tools/recovery_l10n/res/values-eu/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ez dago agindurik"</string> <string name="recovery_error" msgid="5748178989622716736">"Errorea"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Segurtasun-eguneratzea instalatzen"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ezin da kargatu Android sistema. Zure datuak hondatuta egon daitezke. Mezu hau jasotzen jarraitzen baduzu, jatorrizko datuak berrezarri beharko dituzu eta gailuan gordetako erabiltzaile-datu guztiak ezabatu beharko dituzu."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Saiatu berriro"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Berrezarri jatorrizko datuak"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Erabiltzailearen datu guztiak xahutu nahi dituzu?\n\n EKINTZA HORI EZIN DA DESEGIN!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Utzi"</string> </resources> diff --git a/tools/recovery_l10n/res/values-fa/strings.xml b/tools/recovery_l10n/res/values-fa/strings.xml index cc390ae84..1c1be9ae3 100644 --- a/tools/recovery_l10n/res/values-fa/strings.xml +++ b/tools/recovery_l10n/res/values-fa/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"فرمانی وجود ندارد"</string> <string name="recovery_error" msgid="5748178989622716736">"خطا!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"در حال نصب بهروزرسانی امنیتی"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"نمیتوان سیستم Android را بارگیری کرد. ممکن است دادههای شما خراب باشند. اگر همچنان این پیام را دریافت میکنید، شاید لازم باشد بازنشانی دادههای کارخانهای انجام دهید و همه دادههای کاربر را که در این دستگاه ذخیره شده است پاک کنید."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"تلاش مجدد"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"بازنشانی دادههای کارخانه"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"همه دادههای کاربر پاک شود؟\n\n این کار قابلواگرد نیست!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"لغو"</string> </resources> diff --git a/tools/recovery_l10n/res/values-fi/strings.xml b/tools/recovery_l10n/res/values-fi/strings.xml index 5141642c8..fddaf1453 100644 --- a/tools/recovery_l10n/res/values-fi/strings.xml +++ b/tools/recovery_l10n/res/values-fi/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ei komentoa"</string> <string name="recovery_error" msgid="5748178989622716736">"Virhe!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Asennetaan tietoturvapäivitystä"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android-järjestelmän lataaminen epäonnistui. Datasi voi olla vioittunut. Jos näet tämän viestin toistuvasti, sinun on ehkä palautettava tehdasasetukset ja poistettava kaikki laitteella olevat käyttäjätiedot."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Yritä uudelleen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tehdasasetuksien palauttaminen"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Poistetaanko kaikki käyttäjätiedot?\n\nTÄTÄ EI VOI PERUA!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Peruuta"</string> </resources> diff --git a/tools/recovery_l10n/res/values-fr-rCA/strings.xml b/tools/recovery_l10n/res/values-fr-rCA/strings.xml index b2415290b..978e9ff93 100644 --- a/tools/recovery_l10n/res/values-fr-rCA/strings.xml +++ b/tools/recovery_l10n/res/values-fr-rCA/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Aucune commande"</string> <string name="recovery_error" msgid="5748178989622716736">"Erreur!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installation de la mise à jour de sécurité en cours..."</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossible de charger le système Android. Il se peut que vos données soient corrompues. Si vous continuez de recevoir ce message, vous devrez peut-être effectuer une réinitialisation de l\'appareil à ses paramètres d\'usine et effacer toutes les données d\'utilisateur qu\'il contient."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Réessayer"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Réinitialiser aux paramètres d\'usine"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Effacer toutes les données de l\'utilisateur?\n\n CETTE ACTION NE PEUT PAS ÊTRE ANNULÉE!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuler"</string> </resources> diff --git a/tools/recovery_l10n/res/values-fr/strings.xml b/tools/recovery_l10n/res/values-fr/strings.xml index f0472b5ac..693a5ddb4 100644 --- a/tools/recovery_l10n/res/values-fr/strings.xml +++ b/tools/recovery_l10n/res/values-fr/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Aucune commande"</string> <string name="recovery_error" msgid="5748178989622716736">"Erreur !"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installation de la mise à jour de sécurité…"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossible de charger le système Android. Vos données sont peut-être corrompues. Si vous continuez à recevoir ce message, vous devrez peut-être rétablir la configuration d\'usine de votre appareil et effacer toutes les données utilisateur stockées sur cet appareil."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Réessayer"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rétablir la configuration d\'usine"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Effacer toutes les données utilisateur ?\n\n CETTE ACTION NE PEUT PAS ÊTRE ANNULÉE."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuler"</string> </resources> diff --git a/tools/recovery_l10n/res/values-gl/strings.xml b/tools/recovery_l10n/res/values-gl/strings.xml index 42b2016c2..e51b36dfb 100644 --- a/tools/recovery_l10n/res/values-gl/strings.xml +++ b/tools/recovery_l10n/res/values-gl/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Non hai ningún comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Erro"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando actualización de seguranza"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Non se puido cargar o sistema Android. Os teus datos poden estar danados. Se segue aparecendo esta mensaxe, pode ser necesario restablecer os datos de fábrica e borrar todos os datos de usuario almacenados neste dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Tentar de novo"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Restablecemento dos datos de fábrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Queres borrar todos os datos de usuario?\n\n ESTA ACCIÓN NON SE PODE DESFACER."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-gu/strings.xml b/tools/recovery_l10n/res/values-gu/strings.xml index 2355a0f4f..bd83447d7 100644 --- a/tools/recovery_l10n/res/values-gu/strings.xml +++ b/tools/recovery_l10n/res/values-gu/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"કોઈ આદેશ નથી"</string> <string name="recovery_error" msgid="5748178989622716736">"ભૂલ!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"સુરક્ષા અપડેટ ઇન્સ્ટૉલ કરી રહ્યાં છે"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android સિસ્ટમ લોડ કરી શકાતી નથી. તમારો ડેટા કદાચ દૂષિત થયો હોઈ શકે છે. જો તમને આ સંદેશ મળવાનું ચાલુ રહે, તો કદાચ તમારે આ ડિવાઇસ માટે ફેક્ટરી ડેટા રીસેટ કરવાની પ્રક્રિયા કરવી અને આના પર સ્ટોર કરેલો વપરાશકર્તાનો બધો ડેટા કાઢી નાખવો જરૂરી રહેશે."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ફરી પ્રયાસ કરો"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ફેક્ટરી ડેટા રીસેટ કરો"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"શું વપરાશકર્તાનો બધો ડેટા વાઇપ કરીએ?\n\n આ ક્રિયામાં કરેલો ફેરફાર રદ કરી શકાતો નથી!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"રદ કરો"</string> </resources> diff --git a/tools/recovery_l10n/res/values-hi/strings.xml b/tools/recovery_l10n/res/values-hi/strings.xml index 65d003352..c1aa2e97f 100644 --- a/tools/recovery_l10n/res/values-hi/strings.xml +++ b/tools/recovery_l10n/res/values-hi/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"कोई निर्देश नहीं मिला"</string> <string name="recovery_error" msgid="5748178989622716736">"गड़बड़ी!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा अपडेट इंस्टॉल किया जा रहा है"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android सिस्टम लोड नहीं किया जा सकता. शायद आपके डेटा में गड़बड़ी है. अगर आपको यह मैसेज मिलता रहता है, तो शायद आपको फ़ैक्ट्री डेटा रीसेट करना पड़े और इस डिवाइस की मेमोरी में मौजूद उपयोगकर्ता का सभी डेटा हमेशा के लिए मिटाना पड़े."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"फिर से कोशिश करें"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फ़ैक्ट्री डेटा रीसेट"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"क्या उपयोगकर्ता का सभी डेटा मिटाएं?\n\n इसे वापस नहीं लाया जा सकता!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"अभी नहीं"</string> </resources> diff --git a/tools/recovery_l10n/res/values-hr/strings.xml b/tools/recovery_l10n/res/values-hr/strings.xml index 3b75ff115..0fa8fa9fe 100644 --- a/tools/recovery_l10n/res/values-hr/strings.xml +++ b/tools/recovery_l10n/res/values-hr/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nema naredbe"</string> <string name="recovery_error" msgid="5748178989622716736">"Pogreška!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instaliranje sigurnosnog ažuriranja"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sustav Android ne može se učitati. Podaci su možda oštećeni. Ako opet primite ovu poruku, možda ćete morati vratiti uređaj na tvorničko stanje i izbrisati sve podatke korisnika pohranjene na ovom uređaju."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Pokušaj ponovo"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Vraćanje na tvorničko stanje"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite li izbrisati sve podatke korisnika?\n\n TO SE NE MOŽE PONIŠTITI!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Odustani"</string> </resources> diff --git a/tools/recovery_l10n/res/values-hu/strings.xml b/tools/recovery_l10n/res/values-hu/strings.xml index 12d4d9fe7..b7998cea3 100644 --- a/tools/recovery_l10n/res/values-hu/strings.xml +++ b/tools/recovery_l10n/res/values-hu/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nincs parancs"</string> <string name="recovery_error" msgid="5748178989622716736">"Hiba!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Biztonsági frissítés telepítése"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nem sikerült az Android rendszer betöltése. Az adatok sérültek lehetnek. Ha újra megjelenik ez az üzenet, előfordulhat, hogy vissza kell állítania az eszköz gyári adatait, és törölnie kell az eszközön tárolt összes felhasználói adatot."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Újra"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gyári adatok visszaállítása"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Törli az összes felhasználói adatot?\n\n A MŰVELET NEM VONHATÓ VISSZA."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Mégse"</string> </resources> diff --git a/tools/recovery_l10n/res/values-hy/strings.xml b/tools/recovery_l10n/res/values-hy/strings.xml index 9d62bb763..35a0ab113 100644 --- a/tools/recovery_l10n/res/values-hy/strings.xml +++ b/tools/recovery_l10n/res/values-hy/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Հրամանը տրված չէ"</string> <string name="recovery_error" msgid="5748178989622716736">"Սխալ"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Անվտանգության թարմացման տեղադրում"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Չհաջողվեց բեռնել Android համակարգը։ Հնարավոր է՝ ձեր տվյալները վնասված են։ Եթե նորից տեսնեք այս հաղորդագրությունը, փորձեք վերակայել սարքի կարգավորումները և ջնջել օգտատիրոջ բոլոր տվյալները։"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Նորից փորձել"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Վերակայել բոլոր տվյալները"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Մաքրե՞լ օգտատիրոջ բոլոր տվյալները։\n\n ԱՅՍ ԳՈՐԾՈՂՈՒԹՅՈՒՆԸ ՀՆԱՐԱՎՈՐ ՉԻ ԼԻՆԻ ՀԵՏԱՐԿԵԼ"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Չեղարկել"</string> </resources> diff --git a/tools/recovery_l10n/res/values-in/strings.xml b/tools/recovery_l10n/res/values-in/strings.xml index 0e56e0dd9..15a78ec48 100644 --- a/tools/recovery_l10n/res/values-in/strings.xml +++ b/tools/recovery_l10n/res/values-in/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Tidak ada perintah"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Memasang pembaruan keamanan"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Tidak dapat memuat sistem Android. Data Anda mungkin rusak. Jika terus mendapatkan pesan ini, Anda mungkin perlu melakukan reset ke setelan pabrik dan menghapus semua data pengguna yang disimpan di perangkat ini."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Coba lagi"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Reset ke setelan pabrik"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wipe semua data pengguna?\n\n TINDAKAN INI TIDAK DAPAT DIURUNGKAN!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Batal"</string> </resources> diff --git a/tools/recovery_l10n/res/values-is/strings.xml b/tools/recovery_l10n/res/values-is/strings.xml index 5065b6522..4a6295af2 100644 --- a/tools/recovery_l10n/res/values-is/strings.xml +++ b/tools/recovery_l10n/res/values-is/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Engin skipun"</string> <string name="recovery_error" msgid="5748178989622716736">"Villa!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Setur upp öryggisuppfærslu"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ekki er hægt að hlaða Android kerfi. Gögnin þín kunna að vera skemmd. Ef þessi skilaboð halda áfram að birtast gætirðu þurft að núllstilla og eyða öllum notandagögnum sem eru vistuð í þessu tæki."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Reyna aftur"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Núllstilling"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Viltu eyða öllum notandagögnum?\n\n EKKI ER HÆGT AÐ AFTURKALLA ÞETTA!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Hætta við"</string> </resources> diff --git a/tools/recovery_l10n/res/values-it/strings.xml b/tools/recovery_l10n/res/values-it/strings.xml index 2c0364e60..8bc203c15 100644 --- a/tools/recovery_l10n/res/values-it/strings.xml +++ b/tools/recovery_l10n/res/values-it/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nessun comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Errore!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installazione aggiornamento sicurezza…"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Impossibile caricare il sistema Android. I tuoi dati potrebbero essere danneggiati. Se continui a ricevere questo messaggio, potrebbe essere necessario eseguire un ripristino dei dati di fabbrica e cancellare tutti i dati utente memorizzati su questo dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Riprova"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ripristino dati di fabbrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vuoi cancellare tutti i dati utente?\n\n NON È POSSIBILE ANNULLARE L\'OPERAZIONE."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annulla"</string> </resources> diff --git a/tools/recovery_l10n/res/values-iw/strings.xml b/tools/recovery_l10n/res/values-iw/strings.xml index ea5e6f2c9..8ca3bdf00 100644 --- a/tools/recovery_l10n/res/values-iw/strings.xml +++ b/tools/recovery_l10n/res/values-iw/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"אין פקודה"</string> <string name="recovery_error" msgid="5748178989622716736">"שגיאה!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"מתקין עדכון אבטחה"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"לא ניתן לטעון את מערכת Android. ייתכן שהנתונים שלך פגומים. אם הודעה זו תופיע שוב, ייתכן שיהיה עליך לבצע איפוס לנתוני היצרן ולמחוק את כל נתוני המשתמש ששמורים במכשיר זה."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ניסיון נוסף"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"איפוס לנתוני היצרן"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"לאפס את כל נתוני המשתמש?\n\n לא ניתן לבטל פעולה זו!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ביטול"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ja/strings.xml b/tools/recovery_l10n/res/values-ja/strings.xml index 36e029b0f..3d6637278 100644 --- a/tools/recovery_l10n/res/values-ja/strings.xml +++ b/tools/recovery_l10n/res/values-ja/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"コマンドが指定されていません"</string> <string name="recovery_error" msgid="5748178989622716736">"エラーが発生しました。"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"セキュリティ アップデートをインストールしています"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android システムを読み込めません。データが破損している可能性があります。このメッセージが引き続き表示される場合は、データの初期化を行い、この端末に保存されているすべてのユーザー データを消去することが必要な場合があります。"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"再試行"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"データの初期化"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"すべてのユーザー データをワイプしますか?\n\nこの操作は元に戻せません。"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"キャンセル"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ka/strings.xml b/tools/recovery_l10n/res/values-ka/strings.xml index 6a46b3677..04b8a417f 100644 --- a/tools/recovery_l10n/res/values-ka/strings.xml +++ b/tools/recovery_l10n/res/values-ka/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ბრძანება არ არის"</string> <string name="recovery_error" msgid="5748178989622716736">"წარმოიქმნა შეცდომა!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"მიმდინარეობს უსაფრთხოების განახლების ინსტალაცია"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android სისტემის ჩატვირთვა ვერ მოხერხდა. შესაძლოა თქვენი მონაცემები დაზიანებულია. თუ ამ შეტყობინებას კვლავ მიიღებთ, შეიძლება საჭირო იყოს ქარხნული მონაცემების აღდგენა და ამ მოწყობილობაზე შენახული მომხმარებლის ყველა მონაცემის ამოშლა."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ხელახლა ცდა"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ქარხნული მონაცემების აღდგენა"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"გსურთ მომხმარებლის ყველა მონაცემის ამოშლა?\n\n ამ მოქმედების გაუქმება ვერ მოხერხდება!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"გაუქმება"</string> </resources> diff --git a/tools/recovery_l10n/res/values-kk/strings.xml b/tools/recovery_l10n/res/values-kk/strings.xml index a4bd86e66..3f6aa23da 100644 --- a/tools/recovery_l10n/res/values-kk/strings.xml +++ b/tools/recovery_l10n/res/values-kk/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Пәрмен жоқ"</string> <string name="recovery_error" msgid="5748178989622716736">"Қате!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Қауіпсіздік жаңартуы орнатылуда"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android жүйесі жүктелмейді. Деректеріңіз бүлінген болуы мүмкін. Егер осы хабар қайта шықса, зауыттық деректерді қалпына келтіріп, пайдаланушы деректерін жойып көріңіз."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Қайталау"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Зауыттық деректерді қалпына келтіру"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Пайдаланушының барлық деректері жойылсын ба?\n\n БҰЛ ӘРЕКЕТТІ ҚАЙТАРЫЛМАЙДЫ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Бас тарту"</string> </resources> diff --git a/tools/recovery_l10n/res/values-km/strings.xml b/tools/recovery_l10n/res/values-km/strings.xml index 313c0f457..0cedb6bb7 100644 --- a/tools/recovery_l10n/res/values-km/strings.xml +++ b/tools/recovery_l10n/res/values-km/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"គ្មានពាក្យបញ្ជាទេ"</string> <string name="recovery_error" msgid="5748178989622716736">"កំហុស!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"កំពុងដំឡើងការអាប់ដេតសុវត្ថិភាព"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"មិនអាចផ្ទុកប្រព័ន្ធ Android បានទេ។ ទិន្នន័យរបស់អ្នកអាចនឹងខូច។ ប្រសិនបើអ្នកបន្តទទួលបានសារនេះ អ្នកអាចនឹងត្រូវកំណត់ទិន្នន័យដូចចេញពីរោងចក្រ និងលុបទិន្នន័យទាំងអស់របស់អ្នកប្រើប្រាស់ដែលបានផ្ទុកនៅលើឧបករណ៍នេះ។"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ព្យាយាមម្ដងទៀត"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"កំណត់ទិន្នន័យដូចចេញពីរោងចក្រ"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ឈូសទិន្នន័យទាំងអស់របស់អ្នកប្រើប្រាស់?\n\nសកម្មភាពនេះមិនអាចត្រឡប់វិញបានទេ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"បោះបង់"</string> </resources> diff --git a/tools/recovery_l10n/res/values-kn/strings.xml b/tools/recovery_l10n/res/values-kn/strings.xml index 5bf6260ee..a98f4692a 100644 --- a/tools/recovery_l10n/res/values-kn/strings.xml +++ b/tools/recovery_l10n/res/values-kn/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ಯಾವುದೇ ಆದೇಶವಿಲ್ಲ"</string> <string name="recovery_error" msgid="5748178989622716736">"ದೋಷ!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"ಭದ್ರತೆಯ ಅಪ್ಡೇಟ್ ಸ್ಥಾಪಿಸಲಾಗುತ್ತಿದೆ"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ಸಿಸ್ಟಂ ಅನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಿಲ್ಲ. ನಿಮ್ಮ ಡೇಟಾ ದೋಷಪೂರಿತವಾಗಿರಬಹುದು. ನೀವು ಈ ಸಂದೇಶ ಪಡೆಯುವುದು ಮುಂದುವರಿದರೆ, ನೀವು ಫ್ಯಾಕ್ಟರಿ ಡೇಟಾ ರಿಸೆಟ್ ಮಾಡುವ ಅಗತ್ಯವಿದೆ ಮತ್ತು ಈ ಸಾಧನದಲ್ಲಿ ಸಂಗ್ರಹಿಸಲಾದ ಎಲ್ಲಾ ಬಳಕೆದಾರರ ಡೇಟಾವನ್ನು ಅಳಿಸಬೇಕಾಗುತ್ತದೆ."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ಫ್ಯಾಕ್ಟರಿ ಡೇಟಾ ರಿಸೆಟ್"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ಎಲ್ಲಾ ಬಳಕೆದಾರರ ಡೇಟಾವನ್ನು ಅಳಿಸುವುದೇ?\n\n ಇದನ್ನು ರದ್ದುಗೊಳಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ರದ್ದುಮಾಡಿ"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ko/strings.xml b/tools/recovery_l10n/res/values-ko/strings.xml index aca13bbe7..9067f4c34 100644 --- a/tools/recovery_l10n/res/values-ko/strings.xml +++ b/tools/recovery_l10n/res/values-ko/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"명령어 없음"</string> <string name="recovery_error" msgid="5748178989622716736">"오류!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"보안 업데이트 설치 중"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android 시스템을 로드할 수 없습니다. 데이터가 손상되었을 수 있습니다. 이 메시지가 계속 표시되면 초기화를 실행하여 기기에 저장된 사용자 데이터를 모두 삭제해야 할 수도 있습니다."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"다시 시도"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"초기화"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"사용자 데이터를 모두 삭제하시겠습니까?\n\n 이 작업은 실행취소할 수 없습니다."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"취소"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ky/strings.xml b/tools/recovery_l10n/res/values-ky/strings.xml index 0a6bd783a..1cd69ea84 100644 --- a/tools/recovery_l10n/res/values-ky/strings.xml +++ b/tools/recovery_l10n/res/values-ky/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Буйрук берилген жок"</string> <string name="recovery_error" msgid="5748178989622716736">"Ката!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Коопсуздук жаңыртуусу орнотулууда"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android тутуму жүктөлбөй жатат. Дайындарыңыз бузук болушу мүмкүн. Бул билдирүү дагы деле келе берсе, түзмөктү кайра башынан жөндөп, анда сакталган бардык колдонуучу дайындарын тазалашыңыз керек."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Кайталоо"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Кайра башынан жөндөө"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Колдонуучу дайындарынын баары жашырылсынбы?\n\n МУНУ АРТКА КАЙТАРУУ МҮМКҮН ЭМЕС!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Жок"</string> </resources> diff --git a/tools/recovery_l10n/res/values-lo/strings.xml b/tools/recovery_l10n/res/values-lo/strings.xml index d3dbb3970..4a8142783 100644 --- a/tools/recovery_l10n/res/values-lo/strings.xml +++ b/tools/recovery_l10n/res/values-lo/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ບໍ່ມີຄຳສັ່ງ"</string> <string name="recovery_error" msgid="5748178989622716736">"ຜິດພາດ!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"ກຳລັງຕິດຕັ້ງອັບເດດຄວາມປອດໄພ"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"ບໍ່ສາມາດໂຫຼດລະບົບ Android ໄດ້. ຂໍ້ມູນຂອງທ່ານອາດເສຍຫາຍ. ຫາກທ່ານຍັງໄດ້ຮັບຂໍ້ຄວາມນີ້ຕໍ່ໄປ, ທ່ານອາດຕ້ອງຣີເຊັດເປັນຄ່າຈາກໂຮງງານ ແລະ ລຶບຂໍ້ມູນຜູ້ໃຊ້ທັງໝົດທີ່ຈັດເກັບໄວ້ຢູ່ອຸປະກອນນີ້ອອກ."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ລອງໃໝ່"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ຣີເຊັດຄ່າຈາກໂຮງງານ"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ລຶບລ້າງຂໍ້ມູນຜູ້ໃຊ້ທັງໝົດບໍ?\n\n ຄຳສັ່ງນີ້ຈະບໍ່ສາມາດຍົກເລີກໄດ້!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ຍົກເລີກ"</string> </resources> diff --git a/tools/recovery_l10n/res/values-lt/strings.xml b/tools/recovery_l10n/res/values-lt/strings.xml index d5d5e88fd..f9b7d3917 100644 --- a/tools/recovery_l10n/res/values-lt/strings.xml +++ b/tools/recovery_l10n/res/values-lt/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nėra jokių komandų"</string> <string name="recovery_error" msgid="5748178989622716736">"Klaida!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Diegiamas saugos naujinys"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Negalima įkelti „Android“ sistemos. Duomenys gali būti pažeisti. Jei ir toliau gausite šį pranešimą, jums gali reikėti atkurti gamyklinius duomenis ir ištrinti visus naudotojo duomenis, saugomus šiame įrenginyje."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Bandyti dar kartą"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Gamyklinių duomenų atkūrimas"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Išvalyti visus naudotojo duomenis?\n\n ŠIO VEIKSMO NEGALIMA ANULIUOTI!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Atšaukti"</string> </resources> diff --git a/tools/recovery_l10n/res/values-lv/strings.xml b/tools/recovery_l10n/res/values-lv/strings.xml index d877f6a61..6cf8ce30e 100644 --- a/tools/recovery_l10n/res/values-lv/strings.xml +++ b/tools/recovery_l10n/res/values-lv/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nav nevienas komandas"</string> <string name="recovery_error" msgid="5748178989622716736">"Kļūda!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Notiek drošības atjauninājuma instalēšana"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nevar ielādēt Android sistēmu. Jūsu dati var būt bojāti. Ja šis ziņojums tiek rādīts atkārtoti, iespējams, jums ir jāveic rūpnīcas datu atiestatīšana un jādzēš visi šajā ierīcē saglabātie lietotāja dati."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Mēģināt vēlreiz"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rūpnīcas datu atiestatīšana"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vai dzēst visus lietotāja datus?\n\n ŠO DARBĪBU NEVAR ATSAUKT!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Atcelt"</string> </resources> diff --git a/tools/recovery_l10n/res/values-mk/strings.xml b/tools/recovery_l10n/res/values-mk/strings.xml index 351459730..ff56131f9 100644 --- a/tools/recovery_l10n/res/values-mk/strings.xml +++ b/tools/recovery_l10n/res/values-mk/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Нема наредба"</string> <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Се инсталира безбедносно ажурирање"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не може да се вчита системот Android. Можно е податоците да се оштетени. Ако и понатаму ја примате поракава, можеби ќе треба да извршите ресетирање на фабрички податоци и да ги избришете сите кориснички податоци меморирани на уредов."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Обиди се пак"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ресетирање на фабрички податоци"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Да се избришат ли сите кориснички податоци?\n\n ОВА НЕ МОЖЕ ДА СЕ ВРАТИ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Откажи"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ml/strings.xml b/tools/recovery_l10n/res/values-ml/strings.xml index b506e2530..2b331ac7e 100644 --- a/tools/recovery_l10n/res/values-ml/strings.xml +++ b/tools/recovery_l10n/res/values-ml/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"കമാൻഡ് ഒന്നുമില്ല"</string> <string name="recovery_error" msgid="5748178989622716736">"പിശക്!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"സുരക്ഷാ അപ്ഡേറ്റ് ഇൻസ്റ്റാൾ ചെയ്യുന്നു"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android സിസ്റ്റം ലോഡ് ചെയ്യാനാവില്ല. നിങ്ങളുടെ ഡാറ്റ കേടായിരിക്കാം. ഈ സന്ദേശം തുടർന്നും ലഭിക്കുകയാണെങ്കിൽ, നിങ്ങൾ ഒരു ഫാക്ടറി ഡാറ്റ പുനഃക്രമീകരണം നടത്തേണ്ടതുണ്ട് ഒപ്പം ഈ ഉപകരണത്തിൽ സ്റ്റോർ ചെയ്തിട്ടുള്ള എല്ലാ ഉപയോക്തൃ ഡാറ്റകളും മായ്ക്കേണ്ടതുണ്ട്."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"വീണ്ടും ശ്രമിക്കുക"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ഫാക്ടറി ഡാറ്റ പുനഃക്രമീകരണം"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"എല്ലാ ഉപയോക്തൃ ഡാറ്റകളും മായ്ക്കണോ?\n\n ഇത് പഴയപടിയാക്കാനാവില്ല!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"റദ്ദാക്കുക"</string> </resources> diff --git a/tools/recovery_l10n/res/values-mn/strings.xml b/tools/recovery_l10n/res/values-mn/strings.xml index e3dd2e90e..b0a57ed1a 100644 --- a/tools/recovery_l10n/res/values-mn/strings.xml +++ b/tools/recovery_l10n/res/values-mn/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Тушаал байхгүй"</string> <string name="recovery_error" msgid="5748178989622716736">"Алдаа!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Аюулгүй байдлын шинэчлэлтийг суулгаж байна"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Андройд системийг ачаалах боломжгүй байна. Таны өгөгдөл эвдэрч болзошгүй. Хэрэв та энэ мессежийг үргэлжлүүлэн авах бол үйлдвэрээс гарсан төлөвийг ажиллуулж, энэ төхөөрөмжид хадгалсан хэрэглэгчийн бүх өгөгдлийг устгах шаардлагатай байж болзошгүй."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Дахин оролдох"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Үйлдвэрээс гарсан төлөвт"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Хэрэглэгчийн бүх өгөгдлийг арчих уу?\n\n ҮҮНИЙГ БУЦААХ БОЛОМЖГҮЙ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Цуцлах"</string> </resources> diff --git a/tools/recovery_l10n/res/values-mr/strings.xml b/tools/recovery_l10n/res/values-mr/strings.xml index 5f820336f..9b1370794 100644 --- a/tools/recovery_l10n/res/values-mr/strings.xml +++ b/tools/recovery_l10n/res/values-mr/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"कोणतीही कमांड नाही"</string> <string name="recovery_error" msgid="5748178989622716736">"एरर!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा अपडेट इंस्टॉल करत आहे"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android सिस्टम लोड करू शकत नाही. तुमचा डेटा धोक्यात असू शकतो.तुम्हाला हा मेसेज मिळत राहिल्यास, फॅक्टरी डेटा रीसेट करणे आणि या डिव्हाइसवर स्टोअर केलेला सर्व वापरकर्ता डेटा मिटवणे आवश्यक आहे."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"पुन्हा प्रयत्न करा"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फॅक्टरी डेटा रीसेट"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"सर्व वापरकर्ता डेटा पुसून टाकायचा का?\n\n हे पहिल्यासारखे करू शकत नाही!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"रद्द करा"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ms/strings.xml b/tools/recovery_l10n/res/values-ms/strings.xml index 0e24ac4e1..d094f547b 100644 --- a/tools/recovery_l10n/res/values-ms/strings.xml +++ b/tools/recovery_l10n/res/values-ms/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Tiada perintah"</string> <string name="recovery_error" msgid="5748178989622716736">"Ralat!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Memasang kemas kini keselamatan"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Tidak dapat memuatkan sistem Android. Data anda mungkin rosak. Jika anda menerima mesej ini secara berterusan, anda mungkin perlu melaksanakan tetapan semula data kilang dan memadamkan semua data pengguna yang disimpan pada peranti ini."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Cuba lagi"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tetapan semula data kilang"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Lapkan semua data pengguna?\n\n TINDAKAN INI TIDAK BOLEH DIBUAT ASAL!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Batal"</string> </resources> diff --git a/tools/recovery_l10n/res/values-my/strings.xml b/tools/recovery_l10n/res/values-my/strings.xml index f13752461..09cd4ea51 100644 --- a/tools/recovery_l10n/res/values-my/strings.xml +++ b/tools/recovery_l10n/res/values-my/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ညွှန်ကြားချက်မပေးထားပါ"</string> <string name="recovery_error" msgid="5748178989622716736">"မှားနေပါသည်!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"လုံခြုံရေး အပ်ဒိတ်ကို ထည့်သွင်းနေသည်"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android စနစ် ဖွင့်၍မရပါ။ သင့်ဒေတာများ ပျက်နေခြင်း ဖြစ်နိုင်သည်။ ဤမက်ဆေ့ဂျ် ဆက်လက်ရရှိနေလျှင် စက်ရုံထုတ်အခြေအနေပြန်ယူပြီး ဤစက်ပေါ်တွင် သိမ်းထားသော အသုံးပြုသူဒေတာအားလုံး ဖျက်ရန် လိုအပ်နိုင်သည်။"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ထပ်စမ်းကြည့်ပါ"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"စက်ရုံထုတ်အခြေအနေပြန်ယူခြင်း"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"အသုံးပြုသူဒေတာ အားလုံးကို ရှင်းလင်းမလား။\n\n ၎င်းကို ပြန်ပြင်၍မရပါ။"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"မလုပ်တော့"</string> </resources> diff --git a/tools/recovery_l10n/res/values-nb/strings.xml b/tools/recovery_l10n/res/values-nb/strings.xml index ad6f20e46..e8cad136c 100644 --- a/tools/recovery_l10n/res/values-nb/strings.xml +++ b/tools/recovery_l10n/res/values-nb/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ingen kommandoer"</string> <string name="recovery_error" msgid="5748178989622716736">"Feil!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Installerer sikkerhetsoppdateringen"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan ikke laste inn Android-systemet. Dataene dine er muligens skadet. Hvis du fortsetter å se denne meldingen, må du muligens tilbakestille til fabrikkstandard og tømme alle brukerdataene som er lagret på denne enheten."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Prøv igjen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Tilbakestill til fabrikkstandard"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vil du viske ut alle brukerdataene?\n\n DETTE KAN IKKE ANGRES!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Avbryt"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ne/strings.xml b/tools/recovery_l10n/res/values-ne/strings.xml index 1880e807b..fa53e9dae 100644 --- a/tools/recovery_l10n/res/values-ne/strings.xml +++ b/tools/recovery_l10n/res/values-ne/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"कुनै आदेश छैन"</string> <string name="recovery_error" msgid="5748178989622716736">"त्रुटि!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"सुरक्षा सम्बन्धी अद्यावधिकलाई स्थापना गर्दै"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android प्रणाली लोड गर्न सकिएन। तपाईंको डेटा बिग्रेको हुन सक्छ। तपाईं यो सन्देश प्राप्त गर्नुहुन्छ भने तपाईंले फ्याक्ट्री डेटा रिसेट गर्न आवश्यक छ र यो यन्त्रमा भण्डारण गरेका सबै प्रयोगकर्ताको डेटा मेट्न पर्छ।"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"फेरि प्रयास गर्नुहोस्"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"फ्याक्ट्री डेटा रिसेट"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"प्रयोगकर्ताको सबै डेटा मेट्ने हो?\n\n यो अन्डू गर्न सकिँदैन!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"रद्द गर्नुहोस्"</string> </resources> diff --git a/tools/recovery_l10n/res/values-nl/strings.xml b/tools/recovery_l10n/res/values-nl/strings.xml index 0d6c15abb..b42bb6582 100644 --- a/tools/recovery_l10n/res/values-nl/strings.xml +++ b/tools/recovery_l10n/res/values-nl/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Geen opdracht"</string> <string name="recovery_error" msgid="5748178989622716736">"Fout!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Beveiligingsupdate installeren"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Kan het Android-systeem niet laden. Je gegevens zijn mogelijk beschadigd. Als je dit bericht blijft ontvangen, moet je mogelijk de fabrieksinstellingen terugzetten en alle gebruikersgegevens wissen die op dit apparaat zijn opgeslagen."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Opnieuw proberen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Terugzetten op fabrieksinstellingen"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Alle gebruikersgegevens wissen?\n\n DIT KAN NIET ONGEDAAN WORDEN GEMAAKT."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Annuleren"</string> </resources> diff --git a/tools/recovery_l10n/res/values-or/strings.xml b/tools/recovery_l10n/res/values-or/strings.xml index 2b0851cdd..25b28e65a 100644 --- a/tools/recovery_l10n/res/values-or/strings.xml +++ b/tools/recovery_l10n/res/values-or/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"କୌଣସି କମାଣ୍ଡ ନାହିଁ"</string> <string name="recovery_error" msgid="5748178989622716736">"ତ୍ରୁଟି!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"ସୁରକ୍ଷା ଅପ୍ଡେଟ୍ ଇନ୍ଷ୍ଟଲ୍ କରୁଛି"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ସିଷ୍ଟମ୍ ଲୋଡ୍ କରାଯାଇପାରିବ ନାହିଁ। ଆପଣଙ୍କ ଡାଟା ହୁଏତ ତ୍ରୁଟି ରହିଥାଇ ପାରେ। ଯଦି ଆପଣ ଏହି ମେସେଜ୍ ପାଇବା ଜାରି ରଖନ୍ତି, ତେବେ ଆପଣଙ୍କୁ ଫ୍ୟାକ୍ଟେରୀ ଡାଟା ରିସେଟ୍ କରିବାକୁ ହେବ ଏବଂ ଏହି ଡିଭାଇସ୍ରେ ଷ୍ଟୋର୍ ହୋଇଥିବା ସମସ୍ତ ଡାଟା ଇରେଜ୍ କରନ୍ତୁ।"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ପୁଣି ଚେଷ୍ଟା କରନ୍ତୁ"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ଫ୍ୟାକ୍ଟୋରୀ ଡାଟା ରିସେଟ୍"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ସମସ୍ଯ ଉପଯୋଗକର୍ତ୍ତା ଡାଟା ୱାଇପ୍ କରିବେ?\n\n ଏହା ଫେରାଇ ନିଆଯାଇପାରିବ ନାହିଁ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ବାତିଲ୍ କରନ୍ତୁ"</string> </resources> diff --git a/tools/recovery_l10n/res/values-pa/strings.xml b/tools/recovery_l10n/res/values-pa/strings.xml index 27972d117..374306805 100644 --- a/tools/recovery_l10n/res/values-pa/strings.xml +++ b/tools/recovery_l10n/res/values-pa/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ਕੋਈ ਆਦੇਸ਼ ਨਹੀਂ"</string> <string name="recovery_error" msgid="5748178989622716736">"ਅਸ਼ੁੱਧੀ!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"ਸੁਰੱਖਿਆ ਅੱਪਡੇਟ ਸਥਾਪਤ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android ਸਿਸਟਮ ਨੂੰ ਲੋਡ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ। ਸ਼ਾਇਦ ਤੁਹਾਡਾ ਡਾਟਾ ਖਰਾਬ ਹੈ। ਜੇਕਰ ਤੁਹਾਨੂੰ ਇਹ ਸੁਨੇਹਾ ਪ੍ਰਾਪਤ ਹੋਣਾ ਜਾਰੀ ਰਹਿੰਦਾ ਹੈ, ਤਾਂ ਸ਼ਾਇਦ ਤੁਹਾਨੂੰ ਫੈਕਟਰੀ ਡਾਟਾ ਰੀਸੈੱਟ ਕਰਨਾ ਪਵੇ ਅਤੇ ਇਸ ਡੀਵਾਈਸ \'ਤੇ ਸਟੋਰ ਕੀਤੇ ਸਾਰੇ ਵਰਤੋਂਕਾਰ ਡਾਟੇ ਨੂੰ ਮਿਟਾਉਣਾ ਪਵੇ।"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ ਕਰੋ"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ਫੈਕਟਰੀ ਡਾਟਾ ਰੀਸੈੱਟ ਕਰੋ"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ਕੀ ਸਾਰਾ ਵਰਤੋਂਕਾਰ ਡਾਟਾ ਸਾਫ਼ ਕਰਨਾ ਹੈ?\n\n ਇਸਨੂੰ ਅਣਕੀਤਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ਰੱਦ ਕਰੋ"</string> </resources> diff --git a/tools/recovery_l10n/res/values-pl/strings.xml b/tools/recovery_l10n/res/values-pl/strings.xml index 8d6db388d..48d3dbf6e 100644 --- a/tools/recovery_l10n/res/values-pl/strings.xml +++ b/tools/recovery_l10n/res/values-pl/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Brak polecenia"</string> <string name="recovery_error" msgid="5748178989622716736">"Błąd"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instaluję aktualizację zabezpieczeń"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nie można załadować systemu Android. Dane mogą być uszkodzone. Jeśli ten komunikat nadal będzie się pojawiać, może być konieczne przywrócenie danych fabrycznych urządzenia i usunięcie wszystkich zapisanych na nim danych użytkownika."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Ponów próbę"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Przywracanie danych fabrycznych"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Wyczyścić wszystkie dane użytkownika?\n\n TEJ CZYNNOŚCI NIE MOŻNA COFNĄĆ."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anuluj"</string> </resources> diff --git a/tools/recovery_l10n/res/values-pt-rBR/strings.xml b/tools/recovery_l10n/res/values-pt-rBR/strings.xml index b72704385..0df3edccb 100644 --- a/tools/recovery_l10n/res/values-pt-rBR/strings.xml +++ b/tools/recovery_l10n/res/values-pt-rBR/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando atualização de segurança"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Seus dados podem estar corrompidos. Se você continuar recebendo esta mensagem, talvez seja necessário realizar uma redefinição para a configuração original e limpar todos os dados do usuário armazenados neste dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Redefinição para configuração original"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Limpar todos os dados do usuário?\n\n NÃO É POSSÍVEL DESFAZER ESSA AÇÃO."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-pt-rPT/strings.xml b/tools/recovery_l10n/res/values-pt-rPT/strings.xml index 981463739..08eb3c953 100644 --- a/tools/recovery_l10n/res/values-pt-rPT/strings.xml +++ b/tools/recovery_l10n/res/values-pt-rPT/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"A instalar atualização de segurança"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Os seus dados podem estar danificados. Se continuar a receber esta mensagem, pode ter de efetuar uma reposição de dados de fábrica e apagar todos os dados do utilizador armazenados neste dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Reposição de dados de fábrica"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Pretende limpar todos os dados do utilizador?\n\n NÃO É POSSÍVEL ANULAR ESTA AÇÃO."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-pt/strings.xml b/tools/recovery_l10n/res/values-pt/strings.xml index b72704385..0df3edccb 100644 --- a/tools/recovery_l10n/res/values-pt/strings.xml +++ b/tools/recovery_l10n/res/values-pt/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nenhum comando"</string> <string name="recovery_error" msgid="5748178989622716736">"Erro!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Instalando atualização de segurança"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Não é possível carregar o sistema Android. Seus dados podem estar corrompidos. Se você continuar recebendo esta mensagem, talvez seja necessário realizar uma redefinição para a configuração original e limpar todos os dados do usuário armazenados neste dispositivo."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Tentar novamente"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Redefinição para configuração original"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Limpar todos os dados do usuário?\n\n NÃO É POSSÍVEL DESFAZER ESSA AÇÃO."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Cancelar"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ro/strings.xml b/tools/recovery_l10n/res/values-ro/strings.xml index 8032865b8..585db8355 100644 --- a/tools/recovery_l10n/res/values-ro/strings.xml +++ b/tools/recovery_l10n/res/values-ro/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nicio comandă"</string> <string name="recovery_error" msgid="5748178989622716736">"Eroare!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Se instalează actualizarea de securitate"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Nu se poate încărca sistemul Android. Datele dvs. pot fi corupte. Dacă primiți în continuare acest mesaj, poate fi necesar să reveniți la setările din fabrică și să ștergeți toate datele utilizatorului stocate pe acest dispozitiv."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Reîncercați"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Revenire la setările din fabrică"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ștergeți toate datele utilizatorului?\n\n ACEST LUCRU NU POATE FI ANULAT!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anulați"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ru/strings.xml b/tools/recovery_l10n/res/values-ru/strings.xml index feebecf31..db8b7611b 100644 --- a/tools/recovery_l10n/res/values-ru/strings.xml +++ b/tools/recovery_l10n/res/values-ru/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Команды нет"</string> <string name="recovery_error" msgid="5748178989622716736">"Ошибка"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Установка обновления системы безопасности…"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не удалось загрузить систему Android. Возможно, данные повреждены. Если вы снова увидите это сообщение, попробуйте сбросить настройки устройства и удалить все пользовательские данные."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Повторить попытку"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Сбросить настройки"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Стереть все пользовательские данные?\n\nЭТО ДЕЙСТВИЕ НЕЛЬЗЯ ОТМЕНИТЬ."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Отмена"</string> </resources> diff --git a/tools/recovery_l10n/res/values-si/strings.xml b/tools/recovery_l10n/res/values-si/strings.xml index 456cdc567..67aca72f8 100644 --- a/tools/recovery_l10n/res/values-si/strings.xml +++ b/tools/recovery_l10n/res/values-si/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"විධානයක් නොමැත"</string> <string name="recovery_error" msgid="5748178989622716736">"දෝෂය!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"ආරක්ෂක යාවත්කාලීනය ස්ථාපනය කරමින්"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android පද්ධතිය පූරණය කළ නොහැකිය. ඔබේ දත්ත දූෂිත විය හැකිය. ඔබට මෙම පණිවිඩය දිගටම ලැබෙන්නේ නම්, කර්මාන්ත ශාලා දත්ත යළි සැකසීමක් සිදු කර මෙම උපාංගයේ ගබඩා කළ සියලු පරිශීලක දත්ත මකා දැමීමට ඔබට අවශ්ය විය හැකිය."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"නැවත උත්සාහ කරන්න"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"කර්මාන්ත ශාලා දත්ත යළි සැකසීම"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"සියලු පරිශීලක දත්ත මකා දමන්නද?\n\n මෙය පසුගමනය කළ නොහැකිය!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"අවලංගු කරන්න"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sk/strings.xml b/tools/recovery_l10n/res/values-sk/strings.xml index b15f3802b..8a2d2e0c1 100644 --- a/tools/recovery_l10n/res/values-sk/strings.xml +++ b/tools/recovery_l10n/res/values-sk/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Žiadny príkaz"</string> <string name="recovery_error" msgid="5748178989622716736">"Chyba!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Inštaluje sa bezpečnostná aktualizácia"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Systém Android sa nedá načítať. Vaše údaje môžu byť poškodené. Ak chcete získať túto správu a budete pokračovať, zrejme budete musieť obnoviť výrobné nastavenia a vymazať tak všetky údaje používateľa uložené v tomto zariadení."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Skúsiť znova"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Obnovenie výrobných nastavení"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Chcete vymazať všetky údaje používateľa?\n\n TÁTO AKCIA SA NEDÁ VRÁTIŤ SPÄŤ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Zrušiť"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sl/strings.xml b/tools/recovery_l10n/res/values-sl/strings.xml index d608b7506..653c4274e 100644 --- a/tools/recovery_l10n/res/values-sl/strings.xml +++ b/tools/recovery_l10n/res/values-sl/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Ni ukaza"</string> <string name="recovery_error" msgid="5748178989622716736">"Napaka"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Nameščanje varnostne posodobitve"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sistema Android ni mogoče naložiti. Podatki so morda poškodovani. Če se bo to sporočilo še naprej prikazovalo, boste morda morali izvesti ponastavitev na tovarniške nastavitve in izbrisati vse uporabniške podatke, ki so shranjeni v tej napravi."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Poskusi znova"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ponastavitev na tovarniške nastavitve"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Želite izbrisati vse uporabniške podatke?\n\n TEGA NI MOGOČE RAZVELJAVITI!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Prekliči"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sq/strings.xml b/tools/recovery_l10n/res/values-sq/strings.xml index 1156931fb..5c824e683 100644 --- a/tools/recovery_l10n/res/values-sq/strings.xml +++ b/tools/recovery_l10n/res/values-sq/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Nuk ka komanda"</string> <string name="recovery_error" msgid="5748178989622716736">"Gabim!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Po instalon përditësimin e sigurisë"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Sistemi Android nuk mund të ngarkohet. Të dhënat e tua mund të jenë të dëmtuara. Nëse vazhdon të marrësh këtë mesazh, mund të jetë e nevojshme të kryesh një rivendosje të të dhënave të fabrikës dhe të spastrosh të gjitha të dhënat e përdoruesit të ruajtura në këtë pajisje."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Provo përsëri"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Rivendosja e të dhënave të fabrikës"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Të pastrohen të gjitha të dhënat e përdoruesit?\n\n KJO NUK MUND TË ZHBËHET!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Anulo"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sr/strings.xml b/tools/recovery_l10n/res/values-sr/strings.xml index a593d8faa..1583beaaf 100644 --- a/tools/recovery_l10n/res/values-sr/strings.xml +++ b/tools/recovery_l10n/res/values-sr/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Нема команде"</string> <string name="recovery_error" msgid="5748178989622716736">"Грешка!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Инсталира се безбедносно ажурирање"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Учитавање Android система није успело. Подаци су можда оштећени. Ако наставите да добијате ову поруку, можда ћете морати да ресетујете уређај на фабричка подешавања и обришете све податке корисника које чувате на њему."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Пробај поново"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ресетовање на фабричка подешавања"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Желите ли да избришете све податке корисника?\n\n ОВО НЕ МОЖЕ ДА СЕ ОПОЗОВЕ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Откажи"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sv/strings.xml b/tools/recovery_l10n/res/values-sv/strings.xml index b33ce253f..cf43b2511 100644 --- a/tools/recovery_l10n/res/values-sv/strings.xml +++ b/tools/recovery_l10n/res/values-sv/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Inget kommando"</string> <string name="recovery_error" msgid="5748178989622716736">"Fel!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Säkerhetsuppdatering installeras"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Det gick inte att läsa in Android-systemet. Data kan ha skadats. Om det här meddelandet visas igen kan du behöva återställa standardinställningarna så att all användardata som sparats på enheten raderas."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Försök igen"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Återställ standardinställningarna"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Vill du rensa bort all användardata?\n\n DET GÅR INTE ATT ÅNGRA DENNA ÅTGÄRD."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Avbryt"</string> </resources> diff --git a/tools/recovery_l10n/res/values-sw/strings.xml b/tools/recovery_l10n/res/values-sw/strings.xml index 156765881..6fa72825a 100644 --- a/tools/recovery_l10n/res/values-sw/strings.xml +++ b/tools/recovery_l10n/res/values-sw/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Hakuna amri"</string> <string name="recovery_error" msgid="5748178989622716736">"Hitilafu fulani imetokea!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Inasakinisha sasisho la usalama"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Imeshindwa kupakia mfumo wa Android. Huenda data yako imeharibika. Kama utandelea kupata ujumbe huu, huenda ukahitaji kurejesha data iliyotoka nayo kiwandani na ufute data yote ya mtumiaji iliyohifadhiwa kwenye kifaa hiki."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Jaribu tena"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Kurejesha data iliyotoka nayo kiwandani"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Ungependa kufuta data yote ya mtumiaji?\n\n KITENDO HIKI HAKIWEZI KUTENDULIWA!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Ghairi"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ta/strings.xml b/tools/recovery_l10n/res/values-ta/strings.xml index d49186d8d..bc370f7bf 100644 --- a/tools/recovery_l10n/res/values-ta/strings.xml +++ b/tools/recovery_l10n/res/values-ta/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"கட்டளை இல்லை"</string> <string name="recovery_error" msgid="5748178989622716736">"பிழை!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"பாதுகாப்புப் புதுப்பிப்பை நிறுவுகிறது"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android சிஸ்டத்தைக் காண்பிக்க இயலவில்லை. உங்களின் தரவு சிதைந்திருக்கலாம். இந்த மெசேஜ் உங்களுக்குத் தொடர்ந்து வந்தால், தரவின் ஆரம்பநிலைக்கு மீட்டமைத்தல் மற்றும் இந்தச் சாதனத்தில் சேமிக்கப்பட்டுள்ள அனைத்துப் பயனர் தரவையும் அழித்தல் ஆகியவற்றைச் செய்ய வேண்டியிருக்கலாம்."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"மீண்டும் முயல்க"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"தரவின் ஆரம்பநிலை மீட்டமைப்பு"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"பயனரின் அனைத்துத் தரவையும் நீக்கவா?\n\n இதைச் செயல்தவிர்க்க இயலாது!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"இல்லை"</string> </resources> diff --git a/tools/recovery_l10n/res/values-te/strings.xml b/tools/recovery_l10n/res/values-te/strings.xml index e35c82bc4..4d521143f 100644 --- a/tools/recovery_l10n/res/values-te/strings.xml +++ b/tools/recovery_l10n/res/values-te/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ఆదేశం లేదు"</string> <string name="recovery_error" msgid="5748178989622716736">"ఎర్రర్ సంభవించింది!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"భద్రతా నవీకరణను ఇన్స్టాల్ చేస్తోంది"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android సిస్టమ్ని లోడ్ చేయడం సాధ్యం కాదు. మీ డేటా పాడై ఉండవచ్చు. మీకు ఈ సందేశం వస్తూనే ఉంటే, మీరు ఫ్యాక్టరీ డేటా రీసెట్ చేసి, పరికరంలో నిల్వ అయిన వినియోగదారు డేటా మొత్తాన్ని తొలగించాల్సి రావచ్చు."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"మళ్లీ ప్రయత్నించు"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"ఫ్యాక్టరీ డేటా రీసెట్"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"వినియోగదారు డేటా మొత్తాన్ని తొలగించాలా?\n\n ఈ చర్యను రద్దు చేయలేరు!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"రద్దు చేయి"</string> </resources> diff --git a/tools/recovery_l10n/res/values-th/strings.xml b/tools/recovery_l10n/res/values-th/strings.xml index 155affea0..83d445dfe 100644 --- a/tools/recovery_l10n/res/values-th/strings.xml +++ b/tools/recovery_l10n/res/values-th/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"ไม่มีคำสั่ง"</string> <string name="recovery_error" msgid="5748178989622716736">"ข้อผิดพลาด!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"กำลังติดตั้งการอัปเดตความปลอดภัย"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"โหลดระบบ Android ไม่ได้ ข้อมูลของคุณอาจเสียหาย หากคุณยังคงได้รับข้อความนี้อยู่ คุณอาจต้องรีเซ็ตข้อมูลเป็นค่าเริ่มต้นและลบข้อมูลผู้ใช้ทั้งหมดที่เก็บอยู่ในอุปกรณ์นี้"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"ลองอีกครั้ง"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"รีเซ็ตข้อมูลเป็นค่าเริ่มต้น"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"ต้องการล้างข้อมูลผู้ใช้ทั้งหมดใช่ไหม\n\n การกระทำนี้จะยกเลิกไม่ได้"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"ยกเลิก"</string> </resources> diff --git a/tools/recovery_l10n/res/values-tl/strings.xml b/tools/recovery_l10n/res/values-tl/strings.xml index 555b42b8d..6621473fd 100644 --- a/tools/recovery_l10n/res/values-tl/strings.xml +++ b/tools/recovery_l10n/res/values-tl/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Walang command"</string> <string name="recovery_error" msgid="5748178989622716736">"Error!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Nag-i-install ng update sa seguridad"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Hindi ma-load ang Android system. Maaaring sira ang iyong data. Kung patuloy mong matatanggap ang mensaheng ito, maaaring kailanganin mong magsagawa ng pag-reset ng factory data at burahin ang lahat ng data ng user na naka-store sa device na ito."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Subukang muli"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Pag-reset ng factory data"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"I-wipe ang lahat ng data ng user?\n\n HINDI ITO MAA-UNDO!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Kanselahin"</string> </resources> diff --git a/tools/recovery_l10n/res/values-tr/strings.xml b/tools/recovery_l10n/res/values-tr/strings.xml index 5387cb2ae..e4eca52d8 100644 --- a/tools/recovery_l10n/res/values-tr/strings.xml +++ b/tools/recovery_l10n/res/values-tr/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Komut yok"</string> <string name="recovery_error" msgid="5748178989622716736">"Hata!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Güvenlik güncellemesi yükleniyor"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android sisteminiz yüklenemedi. Verileriniz bozulmuş olabilir. Bu mesajı almaya devam ederseniz fabrika verilerine sıfırlama işlemi yapmanız ve bu cihazda depolanan tüm kullanıcı verilerini silmeniz gerekebilir."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Tekrar dene"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Fabrika verilerine sıfırla"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Tüm kullanıcı verileri silinsin mi?\n\n BU İŞLEM GERİ ALINAMAZ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"İptal"</string> </resources> diff --git a/tools/recovery_l10n/res/values-uk/strings.xml b/tools/recovery_l10n/res/values-uk/strings.xml index 0c2fa164a..7bd6fecf0 100644 --- a/tools/recovery_l10n/res/values-uk/strings.xml +++ b/tools/recovery_l10n/res/values-uk/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Немає команди"</string> <string name="recovery_error" msgid="5748178989622716736">"Помилка!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Установлюється оновлення системи безпеки"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Не вдається завантажити систему Android. Можливо, ваші дані пошкоджено. Якщо ви далі отримуватимете це повідомлення, можливо, доведеться відновити заводські налаштування й видалити всі дані користувача з цього пристрою."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Повторити"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Відновити заводські налаштування"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Видалити всі дані користувача?\n\n ЦЮ ДІЮ НЕ МОЖНА ВІДМІНИТИ."</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Скасувати"</string> </resources> diff --git a/tools/recovery_l10n/res/values-ur/strings.xml b/tools/recovery_l10n/res/values-ur/strings.xml index 12e32fbc1..da03f1972 100644 --- a/tools/recovery_l10n/res/values-ur/strings.xml +++ b/tools/recovery_l10n/res/values-ur/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"کوئی کمانڈ نہیں ہے"</string> <string name="recovery_error" msgid="5748178989622716736">"خرابی!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"سیکیورٹی اپ ڈیٹ انسٹال ہو رہی ہے"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android سسٹم لوڈ نہیں کیا جا سکتا۔ آپ کا ڈیٹا خراب ہو سکتا ہے۔ اگر آپ کو مستقل یہ پیغام موصول ہوتا ہے تو آپ کو فیکٹری ڈیٹا کی دوبارہ ترتیب انجام دینے اور اس آلہ پر اسٹور کردہ سبھی صارف ڈیٹا کو مٹانے کی ضرورت پڑ سکتی ہے۔"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"دوبارہ کوشش کریں"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"فیکٹری ڈیٹا کی دوبارہ ترتیب"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"سبھی صارف ڈیٹا صاف کریں؟\n\n اسے کالعدم نہیں کیا جا سکتا!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"منسوخ کریں"</string> </resources> diff --git a/tools/recovery_l10n/res/values-uz/strings.xml b/tools/recovery_l10n/res/values-uz/strings.xml index 2c309d646..9bde4c6bf 100644 --- a/tools/recovery_l10n/res/values-uz/strings.xml +++ b/tools/recovery_l10n/res/values-uz/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Buyruq yo‘q"</string> <string name="recovery_error" msgid="5748178989622716736">"Xato!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Xavfsizlik yangilanishi o‘rnatilmoqda"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Android tizimi yuklanmadi. Maʼlumotlaringiz buzuq shekilli. Yana shu xabarni olsangiz, zavod sozlamalarini tiklashingiz va bu qurilmadagi barcha maʼlumotlarni tozalab tashlashingiz lozim."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Qayta urinish"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Zavod sozlamalarini tiklash"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Barcha maʼlumotlar tozalab tashlansinmi?\n\n ULARNI TIKLASH IMKONSIZ!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Bekor qilish"</string> </resources> diff --git a/tools/recovery_l10n/res/values-vi/strings.xml b/tools/recovery_l10n/res/values-vi/strings.xml index c77d0c8c2..3753394e6 100644 --- a/tools/recovery_l10n/res/values-vi/strings.xml +++ b/tools/recovery_l10n/res/values-vi/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Không có lệnh nào"</string> <string name="recovery_error" msgid="5748178989622716736">"Lỗi!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Đang cài đặt bản cập nhật bảo mật"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Không thể tải hệ thống Android. Dữ liệu của bạn có thể bị hỏng. Nếu tiếp tục thấy thông báo này, bạn có thể cần phải thiết lập lại dữ liệu ban đầu và xóa tất cả dữ liệu người dùng lưu trữ trên thiết bị này."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Thử lại"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Thiết lập lại dữ liệu ban đầu"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Xóa sạch tất cả dữ liệu người dùng?\n\n KHÔNG THỂ HOÀN TÁC THAO TÁC NÀY!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Hủy"</string> </resources> diff --git a/tools/recovery_l10n/res/values-zh-rCN/strings.xml b/tools/recovery_l10n/res/values-zh-rCN/strings.xml index e06149791..ab1fdbbc2 100644 --- a/tools/recovery_l10n/res/values-zh-rCN/strings.xml +++ b/tools/recovery_l10n/res/values-zh-rCN/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"无命令"</string> <string name="recovery_error" msgid="5748178989622716736">"出错了!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"正在安装安全更新"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"无法加载 Android 系统。您的数据可能已损坏。如果系统仍然显示这条消息,您可能需要恢复出厂设置,并清空存储在此设备上的所有用户数据。"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"重试"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"恢复出厂设置"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"是否清除所有用户数据?\n\n此操作无法撤消!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string> </resources> diff --git a/tools/recovery_l10n/res/values-zh-rHK/strings.xml b/tools/recovery_l10n/res/values-zh-rHK/strings.xml index ec3315d32..55ce31e93 100644 --- a/tools/recovery_l10n/res/values-zh-rHK/strings.xml +++ b/tools/recovery_l10n/res/values-zh-rHK/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"沒有指令"</string> <string name="recovery_error" msgid="5748178989622716736">"錯誤!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"正在安裝安全性更新"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"無法載入 Android 系統。您的資料可能已損壞。如您繼續收到此訊息,則可能需要將裝置回復原廠設定,並清除儲存在裝置上的所有使用者資料。"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"再試一次"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"回復原廠設定"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"要清除所有使用者資料嗎?\n\n這項操作無法復原!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string> </resources> diff --git a/tools/recovery_l10n/res/values-zh-rTW/strings.xml b/tools/recovery_l10n/res/values-zh-rTW/strings.xml index 78eae2429..0a777a6e3 100644 --- a/tools/recovery_l10n/res/values-zh-rTW/strings.xml +++ b/tools/recovery_l10n/res/values-zh-rTW/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"沒有指令"</string> <string name="recovery_error" msgid="5748178989622716736">"錯誤!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"正在安裝安全性更新"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"無法載入 Android 系統。你的資料可能已經損毀。如果系統持續顯示這則訊息,你可能必須恢復原廠設定,並清除裝置上儲存的所有使用者資料。"</string> + <string name="recovery_try_again" msgid="7168248750158873496">"再試一次"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"恢復原廠設定"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"要抹除所有使用者資料嗎?\n\n請注意,一旦抹除就無法復原!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"取消"</string> </resources> diff --git a/tools/recovery_l10n/res/values-zu/strings.xml b/tools/recovery_l10n/res/values-zu/strings.xml index 6b815e1ab..4667dac40 100644 --- a/tools/recovery_l10n/res/values-zu/strings.xml +++ b/tools/recovery_l10n/res/values-zu/strings.xml @@ -6,4 +6,9 @@ <string name="recovery_no_command" msgid="4465476568623024327">"Awukho umyalo"</string> <string name="recovery_error" msgid="5748178989622716736">"Iphutha!"</string> <string name="recovery_installing_security" msgid="9184031299717114342">"Ifaka isibuyekezo sokuphepha"</string> + <string name="recovery_wipe_data_menu_header" msgid="550255032058254478">"Ayikwazi ukulayisha isistimu ye-Android. Idatha yakho kungenzeka yonakele. Uma uqhubeka ukuthola lo mlayezo, kungenzeka kumele wenze ukusethwa kabusha kwasekuqaleni kwedatha uphinde usule yonke idatha yomsebenzisi egcinwe kule divayisi."</string> + <string name="recovery_try_again" msgid="7168248750158873496">"Zama futhi"</string> + <string name="recovery_factory_data_reset" msgid="7321351565602894783">"Ukuhlela kabusha idatha yasembonini"</string> + <string name="recovery_wipe_data_confirmation" msgid="5439823343348043954">"Sula yonke idatha yomsebenzisi?\n\n LOKHU AKUKWAZI UKUHLEHLISWA!"</string> + <string name="recovery_cancel_wipe_data" msgid="66987687653647384">"Khansela"</string> </resources> diff --git a/tools/recovery_l10n/res/values/strings.xml b/tools/recovery_l10n/res/values/strings.xml index d56d0733c..a557ba8af 100644 --- a/tools/recovery_l10n/res/values/strings.xml +++ b/tools/recovery_l10n/res/values/strings.xml @@ -36,4 +36,36 @@ system is installing a security update. [CHAR LIMIT=60] --> <string name="recovery_installing_security">Installing security update</string> + <!-- Displayed on the screen beneath the recovery titles when the + device enters the recovery mode and prompts a data wipe. [CHAR + LIMIT=400] --> + <string name="recovery_wipe_data_menu_header">Cannot load Android + system. Your data may be corrupt. If you continue to get this + message, you may need to perform a factory data reset and erase + all user data stored on this device.</string> + + <!-- Displayed on the screen as the first element of the menu to + prompt the wipe data, beneath the menu header. The menu shows + up when the device enters the recovery mode and prompts a data + wipe. [CHAR LIMIT=60] --> + <string name="recovery_try_again">Try again</string> + + <!-- Displayed on the screen as the second element of the menu to + prompt the wipe data, beneath the menu header. The menu shows + up when the device enters the recovery mode and prompts a data + wipe. [CHAR LIMIT=60] --> + <string name="recovery_factory_data_reset">Factory data reset</string> + + <!-- Displayed on the screen beneath the recovery titles when users + select "Factory data reset" in the previous menu. [CHAR + LIMIT=150] --> + <string name="recovery_wipe_data_confirmation">Wipe all user data?\n\n + THIS CAN NOT BE UNDONE!</string> + + <!-- Displayed on the screen as the first element of the wipe data + confirmation menu. The menu shows up when users select + "Factory data reset" when prompted to wipe data. [CHAR + LIMIT=60] --> + <string name="recovery_cancel_wipe_data">Cancel</string> + </resources> @@ -59,6 +59,7 @@ RecoveryUI::RecoveryUI() brightness_file_(BRIGHTNESS_FILE), max_brightness_file_(MAX_BRIGHTNESS_FILE), touch_screen_allowed_(false), + fastbootd_logo_enabled_(false), touch_low_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_low_threshold", kDefaultTouchLowThreshold)), touch_high_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_high_threshold", @@ -162,6 +162,24 @@ class RecoveryUI { const std::vector<std::string>& items, size_t initial_selection, bool menu_only, const std::function<int(int, bool)>& key_handler) = 0; + // Displays the localized wipe data menu with pre-generated graphs. If there's an issue + // with the graphs, falls back to use the backup string headers and items instead. The initial + // selection is the 0th item in the menu, which is expected to reboot the device without a wipe. + virtual size_t ShowPromptWipeDataMenu(const std::vector<std::string>& backup_headers, + const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) = 0; + // Displays the localized wipe data confirmation menu with pre-generated images. Falls back to + // the text strings upon failures. The initial selection is the 0th item, which returns to the + // upper level menu. + virtual size_t ShowPromptWipeDataConfirmationMenu( + const std::vector<std::string>& backup_headers, const std::vector<std::string>& backup_items, + const std::function<int(int, bool)>& key_handler) = 0; + + // Set whether or not the fastbootd logo is displayed. + void SetEnableFastbootdLogo(bool enable) { + fastbootd_logo_enabled_ = enable; + } + // Resets the key interrupt status. void ResetKeyInterruptStatus() { key_interrupted_ = false; @@ -186,6 +204,8 @@ class RecoveryUI { // Whether we should listen for touch inputs (default: false). bool touch_screen_allowed_; + bool fastbootd_logo_enabled_; + private: enum class ScreensaverState { DISABLED, diff --git a/uncrypt/uncrypt.cpp b/uncrypt/uncrypt.cpp index 95f40c71f..f1f4f69f0 100644 --- a/uncrypt/uncrypt.cpp +++ b/uncrypt/uncrypt.cpp @@ -89,7 +89,6 @@ #include <errno.h> #include <fcntl.h> #include <inttypes.h> -#include <libgen.h> #include <linux/fs.h> #include <stdarg.h> #include <stdio.h> @@ -103,6 +102,7 @@ #include <algorithm> #include <memory> +#include <string> #include <vector> #include <android-base/file.h> @@ -115,9 +115,13 @@ #include <cutils/android_reboot.h> #include <cutils/sockets.h> #include <fs_mgr.h> +#include <fstab/fstab.h> #include "otautil/error_code.h" +using android::fs_mgr::Fstab; +using android::fs_mgr::ReadDefaultFstab; + static constexpr int WINDOW_SIZE = 5; static constexpr int FIBMAP_RETRY_LIMIT = 3; @@ -136,7 +140,7 @@ static const std::string UNCRYPT_PATH_FILE = "/cache/recovery/uncrypt_file"; static const std::string UNCRYPT_STATUS = "/cache/recovery/uncrypt_status"; static const std::string UNCRYPT_SOCKET = "uncrypt"; -static struct fstab* fstab = nullptr; +static Fstab fstab; static int write_at_offset(unsigned char* buffer, size_t size, int wfd, off64_t offset) { if (TEMP_FAILURE_RETRY(lseek64(wfd, offset, SEEK_SET)) == -1) { @@ -162,49 +166,34 @@ static void add_block_to_ranges(std::vector<int>& ranges, int new_block) { } } -static struct fstab* read_fstab() { - fstab = fs_mgr_read_fstab_default(); - if (!fstab) { - LOG(ERROR) << "failed to read default fstab"; - return NULL; - } - - return fstab; -} - -static const char* find_block_device(const char* path, bool* encryptable, - bool* encrypted, bool* f2fs_fs) { - // Look for a volume whose mount point is the prefix of path and - // return its block device. Set encrypted if it's currently - // encrypted. - - // ensure f2fs_fs is set to false first. - *f2fs_fs = false; - - for (int i = 0; i < fstab->num_entries; ++i) { - struct fstab_rec* v = &fstab->recs[i]; - if (!v->mount_point) { - continue; - } - int len = strlen(v->mount_point); - if (strncmp(path, v->mount_point, len) == 0 && - (path[len] == '/' || path[len] == 0)) { - *encrypted = false; - *encryptable = false; - if (fs_mgr_is_encryptable(v) || fs_mgr_is_file_encrypted(v)) { - *encryptable = true; - if (android::base::GetProperty("ro.crypto.state", "") == "encrypted") { - *encrypted = true; - } - } - if (strcmp(v->fs_type, "f2fs") == 0) { - *f2fs_fs = true; - } - return v->blk_device; +// Looks for a volume whose mount point is the prefix of path and returns its block device or an +// empty string. Sets encryption flags accordingly. +static std::string FindBlockDevice(const std::string& path, bool* encryptable, bool* encrypted, + bool* f2fs_fs) { + // Ensure f2fs_fs is set to false first. + *f2fs_fs = false; + + for (const auto& entry : fstab) { + if (entry.mount_point.empty()) { + continue; + } + if (android::base::StartsWith(path, entry.mount_point + "/")) { + *encrypted = false; + *encryptable = false; + if (entry.is_encryptable() || entry.fs_mgr_flags.file_encryption) { + *encryptable = true; + if (android::base::GetProperty("ro.crypto.state", "") == "encrypted") { + *encrypted = true; } + } + if (entry.fs_type == "f2fs") { + *f2fs_fs = true; + } + return entry.blk_device; } + } - return NULL; + return ""; } static bool write_status_to_socket(int status, int socket) { @@ -217,103 +206,102 @@ static bool write_status_to_socket(int status, int socket) { return android::base::WriteFully(socket, &status_out, sizeof(int)); } -// Parse uncrypt_file to find the update package name. -static bool find_uncrypt_package(const std::string& uncrypt_path_file, std::string* package_name) { - CHECK(package_name != nullptr); - std::string uncrypt_path; - if (!android::base::ReadFileToString(uncrypt_path_file, &uncrypt_path)) { - PLOG(ERROR) << "failed to open \"" << uncrypt_path_file << "\""; - return false; - } - - // Remove the trailing '\n' if present. - *package_name = android::base::Trim(uncrypt_path); - return true; -} - -static int retry_fibmap(const int fd, const char* name, int* block, const int head_block) { - CHECK(block != nullptr); - for (size_t i = 0; i < FIBMAP_RETRY_LIMIT; i++) { - if (fsync(fd) == -1) { - PLOG(ERROR) << "failed to fsync \"" << name << "\""; - return kUncryptFileSyncError; - } - if (ioctl(fd, FIBMAP, block) != 0) { - PLOG(ERROR) << "failed to find block " << head_block; - return kUncryptIoctlError; - } - if (*block != 0) { - return kUncryptNoError; - } - sleep(1); - } - LOG(ERROR) << "fibmap of " << head_block << "always returns 0"; - return kUncryptIoctlError; +// Parses the given path file to find the update package name. +static bool FindUncryptPackage(const std::string& uncrypt_path_file, std::string* package_name) { + CHECK(package_name != nullptr); + std::string uncrypt_path; + if (!android::base::ReadFileToString(uncrypt_path_file, &uncrypt_path)) { + PLOG(ERROR) << "failed to open \"" << uncrypt_path_file << "\""; + return false; + } + + // Remove the trailing '\n' if present. + *package_name = android::base::Trim(uncrypt_path); + return true; } -static int produce_block_map(const char* path, const char* map_file, const char* blk_dev, - bool encrypted, bool f2fs_fs, int socket) { - std::string err; - if (!android::base::RemoveFileIfExists(map_file, &err)) { - LOG(ERROR) << "failed to remove the existing map file " << map_file << ": " << err; - return kUncryptFileRemoveError; - } - std::string tmp_map_file = std::string(map_file) + ".tmp"; - android::base::unique_fd mapfd(open(tmp_map_file.c_str(), - O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR)); - if (mapfd == -1) { - PLOG(ERROR) << "failed to open " << tmp_map_file; - return kUncryptFileOpenError; +static int RetryFibmap(int fd, const std::string& name, int* block, const int head_block) { + CHECK(block != nullptr); + for (size_t i = 0; i < FIBMAP_RETRY_LIMIT; i++) { + if (fsync(fd) == -1) { + PLOG(ERROR) << "failed to fsync \"" << name << "\""; + return kUncryptFileSyncError; } - - // Make sure we can write to the socket. - if (!write_status_to_socket(0, socket)) { - LOG(ERROR) << "failed to write to socket " << socket; - return kUncryptSocketWriteError; + if (ioctl(fd, FIBMAP, block) != 0) { + PLOG(ERROR) << "failed to find block " << head_block; + return kUncryptIoctlError; } - - struct stat sb; - if (stat(path, &sb) != 0) { - LOG(ERROR) << "failed to stat " << path; - return kUncryptFileStatError; + if (*block != 0) { + return kUncryptNoError; } + sleep(1); + } + LOG(ERROR) << "fibmap of " << head_block << " always returns 0"; + return kUncryptIoctlError; +} - LOG(INFO) << " block size: " << sb.st_blksize << " bytes"; - - int blocks = ((sb.st_size-1) / sb.st_blksize) + 1; - LOG(INFO) << " file size: " << sb.st_size << " bytes, " << blocks << " blocks"; - - std::vector<int> ranges; - - std::string s = android::base::StringPrintf("%s\n%" PRId64 " %" PRId64 "\n", - blk_dev, static_cast<int64_t>(sb.st_size), - static_cast<int64_t>(sb.st_blksize)); - if (!android::base::WriteStringToFd(s, mapfd)) { - PLOG(ERROR) << "failed to write " << tmp_map_file; - return kUncryptWriteError; - } - - std::vector<std::vector<unsigned char>> buffers; - if (encrypted) { - buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize)); - } - int head_block = 0; - int head = 0, tail = 0; - - android::base::unique_fd fd(open(path, O_RDONLY)); - if (fd == -1) { - PLOG(ERROR) << "failed to open " << path << " for reading"; - return kUncryptFileOpenError; - } - - android::base::unique_fd wfd; - if (encrypted) { - wfd.reset(open(blk_dev, O_WRONLY)); - if (wfd == -1) { - PLOG(ERROR) << "failed to open " << blk_dev << " for writing"; - return kUncryptBlockOpenError; - } - } +static int ProductBlockMap(const std::string& path, const std::string& map_file, + const std::string& blk_dev, bool encrypted, bool f2fs_fs, int socket) { + std::string err; + if (!android::base::RemoveFileIfExists(map_file, &err)) { + LOG(ERROR) << "failed to remove the existing map file " << map_file << ": " << err; + return kUncryptFileRemoveError; + } + std::string tmp_map_file = map_file + ".tmp"; + android::base::unique_fd mapfd(open(tmp_map_file.c_str(), O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR)); + if (mapfd == -1) { + PLOG(ERROR) << "failed to open " << tmp_map_file; + return kUncryptFileOpenError; + } + + // Make sure we can write to the socket. + if (!write_status_to_socket(0, socket)) { + LOG(ERROR) << "failed to write to socket " << socket; + return kUncryptSocketWriteError; + } + + struct stat sb; + if (stat(path.c_str(), &sb) != 0) { + PLOG(ERROR) << "failed to stat " << path; + return kUncryptFileStatError; + } + + LOG(INFO) << " block size: " << sb.st_blksize << " bytes"; + + int blocks = ((sb.st_size - 1) / sb.st_blksize) + 1; + LOG(INFO) << " file size: " << sb.st_size << " bytes, " << blocks << " blocks"; + + std::vector<int> ranges; + + std::string s = android::base::StringPrintf("%s\n%" PRId64 " %" PRId64 "\n", blk_dev.c_str(), + static_cast<int64_t>(sb.st_size), + static_cast<int64_t>(sb.st_blksize)); + if (!android::base::WriteStringToFd(s, mapfd)) { + PLOG(ERROR) << "failed to write " << tmp_map_file; + return kUncryptWriteError; + } + + std::vector<std::vector<unsigned char>> buffers; + if (encrypted) { + buffers.resize(WINDOW_SIZE, std::vector<unsigned char>(sb.st_blksize)); + } + int head_block = 0; + int head = 0, tail = 0; + + android::base::unique_fd fd(open(path.c_str(), O_RDWR)); + if (fd == -1) { + PLOG(ERROR) << "failed to open " << path << " for reading"; + return kUncryptFileOpenError; + } + + android::base::unique_fd wfd; + if (encrypted) { + wfd.reset(open(blk_dev.c_str(), O_WRONLY)); + if (wfd == -1) { + PLOG(ERROR) << "failed to open " << blk_dev << " for writing"; + return kUncryptBlockOpenError; + } + } // F2FS-specific ioctl // It requires the below kernel commit merged in v4.16-rc1. @@ -329,7 +317,7 @@ static int produce_block_map(const char* path, const char* map_file, const char* #define F2FS_IOCTL_MAGIC 0xf5 #endif #define F2FS_IOC_SET_PIN_FILE _IOW(F2FS_IOCTL_MAGIC, 13, __u32) -#define F2FS_IOC_GET_PIN_FILE _IOW(F2FS_IOCTL_MAGIC, 14, __u32) +#define F2FS_IOC_GET_PIN_FILE _IOR(F2FS_IOCTL_MAGIC, 14, __u32) #endif if (f2fs_fs) { __u32 set = 1; @@ -361,7 +349,7 @@ static int produce_block_map(const char* path, const char* map_file, const char* if (block == 0) { LOG(ERROR) << "failed to find block " << head_block << ", retrying"; - int error = retry_fibmap(fd, path, &block, head_block); + int error = RetryFibmap(fd, path, &block, head_block); if (error != kUncryptNoError) { return error; } @@ -406,7 +394,7 @@ static int produce_block_map(const char* path, const char* map_file, const char* if (block == 0) { LOG(ERROR) << "failed to find block " << head_block << ", retrying"; - int error = retry_fibmap(fd, path, &block, head_block); + int error = RetryFibmap(fd, path, &block, head_block); if (error != kUncryptNoError) { return error; } @@ -456,13 +444,12 @@ static int produce_block_map(const char* path, const char* map_file, const char* } } - if (rename(tmp_map_file.c_str(), map_file) == -1) { - PLOG(ERROR) << "failed to rename " << tmp_map_file << " to " << map_file; - return kUncryptFileRenameError; + if (rename(tmp_map_file.c_str(), map_file.c_str()) == -1) { + PLOG(ERROR) << "failed to rename " << tmp_map_file << " to " << map_file; + return kUncryptFileRenameError; } // Sync dir to make rename() result written to disk. - std::string file_name = map_file; - std::string dir_name = dirname(&file_name[0]); + std::string dir_name = android::base::Dirname(map_file); android::base::unique_fd dfd(open(dir_name.c_str(), O_RDONLY | O_DIRECTORY)); if (dfd == -1) { PLOG(ERROR) << "failed to open dir " << dir_name; @@ -479,45 +466,42 @@ static int produce_block_map(const char* path, const char* map_file, const char* return 0; } -static int uncrypt(const char* input_path, const char* map_file, const int socket) { - LOG(INFO) << "update package is \"" << input_path << "\""; - - // Turn the name of the file we're supposed to convert into an absolute path, so we can find - // what filesystem it's on. - char path[PATH_MAX+1]; - if (realpath(input_path, path) == nullptr) { - PLOG(ERROR) << "failed to convert \"" << input_path << "\" to absolute path"; - return kUncryptRealpathFindError; - } - - bool encryptable; - bool encrypted; - bool f2fs_fs; - const char* blk_dev = find_block_device(path, &encryptable, &encrypted, &f2fs_fs); - if (blk_dev == nullptr) { - LOG(ERROR) << "failed to find block device for " << path; - return kUncryptBlockDeviceFindError; - } - - // If the filesystem it's on isn't encrypted, we only produce the - // block map, we don't rewrite the file contents (it would be - // pointless to do so). - LOG(INFO) << "encryptable: " << (encryptable ? "yes" : "no"); - LOG(INFO) << " encrypted: " << (encrypted ? "yes" : "no"); - - // Recovery supports installing packages from 3 paths: /cache, - // /data, and /sdcard. (On a particular device, other locations - // may work, but those are three we actually expect.) - // - // On /data we want to convert the file to a block map so that we - // can read the package without mounting the partition. On /cache - // and /sdcard we leave the file alone. - if (strncmp(path, "/data/", 6) == 0) { - LOG(INFO) << "writing block map " << map_file; - return produce_block_map(path, map_file, blk_dev, encrypted, f2fs_fs, socket); - } - - return 0; +static int Uncrypt(const std::string& input_path, const std::string& map_file, int socket) { + LOG(INFO) << "update package is \"" << input_path << "\""; + + // Turn the name of the file we're supposed to convert into an absolute path, so we can find what + // filesystem it's on. + std::string path; + if (!android::base::Realpath(input_path, &path)) { + PLOG(ERROR) << "Failed to convert \"" << input_path << "\" to absolute path"; + return kUncryptRealpathFindError; + } + + bool encryptable; + bool encrypted; + bool f2fs_fs; + const std::string blk_dev = FindBlockDevice(path, &encryptable, &encrypted, &f2fs_fs); + if (blk_dev.empty()) { + LOG(ERROR) << "Failed to find block device for " << path; + return kUncryptBlockDeviceFindError; + } + + // If the filesystem it's on isn't encrypted, we only produce the block map, we don't rewrite the + // file contents (it would be pointless to do so). + LOG(INFO) << "encryptable: " << (encryptable ? "yes" : "no"); + LOG(INFO) << " encrypted: " << (encrypted ? "yes" : "no"); + + // Recovery supports installing packages from 3 paths: /cache, /data, and /sdcard. (On a + // particular device, other locations may work, but those are three we actually expect.) + // + // On /data we want to convert the file to a block map so that we can read the package without + // mounting the partition. On /cache and /sdcard we leave the file alone. + if (android::base::StartsWith(path, "/data/")) { + LOG(INFO) << "writing block map " << map_file; + return ProductBlockMap(path, map_file, blk_dev, encrypted, f2fs_fs, socket); + } + + return 0; } static void log_uncrypt_error_code(UncryptErrorCode error_code) { @@ -533,18 +517,18 @@ static bool uncrypt_wrapper(const char* input_path, const char* map_file, const std::string package; if (input_path == nullptr) { - if (!find_uncrypt_package(UNCRYPT_PATH_FILE, &package)) { - write_status_to_socket(-1, socket); - // Overwrite the error message. - log_uncrypt_error_code(kUncryptPackageMissingError); - return false; - } - input_path = package.c_str(); + if (!FindUncryptPackage(UNCRYPT_PATH_FILE, &package)) { + write_status_to_socket(-1, socket); + // Overwrite the error message. + log_uncrypt_error_code(kUncryptPackageMissingError); + return false; + } + input_path = package.c_str(); } CHECK(map_file != nullptr); auto start = std::chrono::system_clock::now(); - int status = uncrypt(input_path, map_file, socket); + int status = Uncrypt(input_path, map_file, socket); std::chrono::duration<double> duration = std::chrono::system_clock::now() - start; int count = static_cast<int>(duration.count()); @@ -654,7 +638,8 @@ int main(int argc, char** argv) { return 2; } - if ((fstab = read_fstab()) == nullptr) { + if (!ReadDefaultFstab(&fstab)) { + LOG(ERROR) << "failed to read default fstab"; log_uncrypt_error_code(kUncryptFstabReadError); return 1; } diff --git a/update_verifier/Android.bp b/update_verifier/Android.bp index 7a860a149..f6567137e 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: [ @@ -43,12 +42,15 @@ cc_library_static { static_libs: [ "libotautil", + "libvold_binder", ], shared_libs: [ "android.hardware.boot@1.0", "libbase", "libcutils", + "libbinder", + "libutils", ], proto: { @@ -71,6 +73,7 @@ cc_binary { static_libs: [ "libupdate_verifier", "libotautil", + "libvold_binder", ], shared_libs: [ @@ -81,6 +84,7 @@ cc_binary { "libhidlbase", "liblog", "libprotobuf-cpp-lite", + "libbinder", "libutils", ], 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..4c64b1ea1 100644 --- a/update_verifier/include/update_verifier/update_verifier.h +++ b/update_verifier/include/update_verifier/update_verifier.h @@ -16,17 +16,56 @@ #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; + // 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..0cf536ce6 100644 --- a/update_verifier/update_verifier.cpp +++ b/update_verifier/update_verifier.cpp @@ -42,14 +42,15 @@ #include <dirent.h> #include <errno.h> #include <fcntl.h> +#include <stdint.h> #include <stdio.h> +#include <stdlib.h> #include <string.h> #include <unistd.h> #include <algorithm> #include <future> -#include <string> -#include <vector> +#include <thread> #include <android-base/file.h> #include <android-base/logging.h> @@ -58,16 +59,21 @@ #include <android-base/strings.h> #include <android-base/unique_fd.h> #include <android/hardware/boot/1.0/IBootControl.h> +#include <android/os/IVold.h> +#include <binder/BinderService.h> +#include <binder/Status.h> #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; +// TODO(xunchang) remove the prefix and use a default path instead. +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 +82,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 +116,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 +135,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 +146,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,26 +179,20 @@ 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) { - // care_map file has up to six lines, where every two lines make a pair. Within each pair, the - // first line has the partition name (e.g. "system"), while the second line holds the ranges of - // all the blocks to verify. - std::vector<std::string> lines = - android::base::Split(android::base::Trim(care_map_contents), "\n"); - if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) { - LOG(ERROR) << "Invalid lines in care_map: found " << lines.size() - << " lines, expecting 2 or 4 or 6 lines."; +bool UpdateVerifier::VerifyPartitions() { + auto dm_block_devices = FindDmPartitions(); + if (dm_block_devices.empty()) { + LOG(ERROR) << "No dm-enabled block device is found."; return false; } - for (size_t i = 0; i < lines.size(); i += 2) { - // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O - // update_verifier (the last few metadata blocks can't be read via device mapper). - if (android::base::StartsWith(lines[i], "/dev/block/")) { - LOG(WARNING) << "Found legacy care_map.txt; skipped."; - return true; + for (const auto& [partition_name, ranges] : partition_map_) { + if (dm_block_devices.find(partition_name) == dm_block_devices.end()) { + LOG(ERROR) << "Failed to find dm block device for " << partition_name; + return false; } - if (!read_blocks(lines[i], lines[i+1])) { + + if (!ReadBlocks(partition_name, dm_block_devices.at(partition_name), ranges)) { return false; } } @@ -217,50 +200,91 @@ static bool process_care_map_plain_text(const std::string& care_map_contents) { 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(ERROR) << care_map_name << " doesn't exist"; + return false; + } + 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. + 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,20 +332,39 @@ 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(); } } - CommandResult cr; - module->markBootSuccessful([&cr](CommandResult result) { cr = result; }); - if (!cr.success) { - LOG(ERROR) << "Error marking booted successfully: " << cr.errMsg; - return reboot_device(); + bool supports_checkpoint = false; + auto sm = android::defaultServiceManager(); + android::sp<android::IBinder> binder = sm->getService(android::String16("vold")); + if (binder) { + auto vold = android::interface_cast<android::os::IVold>(binder); + android::binder::Status status = vold->supportsCheckpoint(&supports_checkpoint); + if (!status.isOk()) { + LOG(ERROR) << "Failed to check if checkpoints supported. Continuing"; + } + } else { + LOG(ERROR) << "Failed to obtain vold Binder. Continuing"; + } + + if (!supports_checkpoint) { + CommandResult cr; + module->markBootSuccessful([&cr](CommandResult result) { cr = result; }); + if (!cr.success) { + LOG(ERROR) << "Error marking booted successfully: " << cr.errMsg; + return reboot_device(); + } + LOG(INFO) << "Marked slot " << current_slot << " as booted successfully."; + } else { + LOG(INFO) << "Deferred marking slot " << current_slot << " as booted successfully."; } - LOG(INFO) << "Marked slot " << current_slot << " as booted successfully."; } LOG(INFO) << "Leaving update_verifier."; diff --git a/updater/Android.bp b/updater/Android.bp index c95ec5e90..b80cdb3a0 100644 --- a/updater/Android.bp +++ b/updater/Android.bp @@ -26,12 +26,14 @@ cc_defaults { "libedify", "libotautil", "libext4_utils", + "libdm", "libfec", "libfec_rs", "libverity_tree", "libfs_mgr", "libgtest_prod", "liblog", + "liblp", "libselinux", "libsparse", "libsquashfs_utils", @@ -66,6 +68,7 @@ cc_library_static { srcs: [ "blockimg.cpp", "commands.cpp", + "dynamic_partitions.cpp", "install.cpp", ], diff --git a/updater/Android.mk b/updater/Android.mk index 78e32ba39..c7a6ba989 100644 --- a/updater/Android.mk +++ b/updater/Android.mk @@ -29,12 +29,14 @@ updater_common_static_libraries := \ libedify \ libotautil \ libext4_utils \ + libdm \ libfec \ libfec_rs \ libverity_tree \ libfs_mgr \ libgtest_prod \ liblog \ + liblp \ libselinux \ libsparse \ libsquashfs_utils \ diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp index 838845673..07c3c7b52 100644 --- a/updater/blockimg.cpp +++ b/updater/blockimg.cpp @@ -53,6 +53,7 @@ #include <ziparchive/zip_archive.h> #include "edify/expr.h" +#include "otautil/dirutil.h" #include "otautil/error_code.h" #include "otautil/paths.h" #include "otautil/print_sha1.h" @@ -69,6 +70,7 @@ static constexpr size_t BLOCKSIZE = 4096; static constexpr mode_t STASH_DIRECTORY_MODE = 0700; static constexpr mode_t STASH_FILE_MODE = 0600; +static constexpr mode_t MARKER_DIRECTORY_MODE = 0700; static CauseCode failure_type = kNoCause; static bool is_retry = false; @@ -109,7 +111,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; } @@ -166,26 +168,37 @@ static bool UpdateLastCommandIndex(size_t command_index, const std::string& comm return true; } -static bool SetPartitionUpdatedMarker(const std::string& marker) { +bool SetUpdatedMarker(const std::string& marker) { + auto dirname = android::base::Dirname(marker); + auto res = mkdir(dirname.c_str(), MARKER_DIRECTORY_MODE); + if (res == -1 && errno != EEXIST) { + PLOG(ERROR) << "Failed to create directory for marker: " << dirname; + return false; + } + if (!android::base::WriteStringToFile("", marker)) { PLOG(ERROR) << "Failed to write to marker file " << marker; return false; } - if (!FsyncDir(android::base::Dirname(marker))) { + if (!FsyncDir(dirname)) { return false; } - LOG(INFO) << "Wrote partition updated marker to " << marker; + LOG(INFO) << "Wrote updated marker to " << marker; return true; } -static bool discard_blocks(int fd, off64_t offset, uint64_t size) { - // Don't discard blocks unless the update is a retry run. - if (!is_retry) { +static bool discard_blocks(int fd, off64_t offset, uint64_t size, bool force = false) { + // Don't discard blocks unless the update is a retry run or force == true + if (!is_retry && !force) { return true; } uint64_t args[2] = { static_cast<uint64_t>(offset), size }; if (ioctl(fd, BLKDISCARD, &args) == -1) { + // On devices that does not support BLKDISCARD, ignore the error. + if (errno == EOPNOTSUPP) { + return true; + } PLOG(ERROR) << "BLKDISCARD ioctl failed"; return false; } @@ -874,7 +887,7 @@ static int CreateStash(State* state, size_t maxblocks, const std::string& base) size_t max_stash_size = maxblocks * BLOCKSIZE; if (res == -1) { LOG(INFO) << "creating stash " << dirname; - res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE); + res = mkdir_recursively(dirname, STASH_DIRECTORY_MODE, false, nullptr); if (res != 0) { ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s", dirname.c_str(), @@ -1399,7 +1412,10 @@ static int PerformCommandDiff(CommandParameters& params) { // We expect the output of the patcher to fill the tgt ranges exactly. if (!writer.Finished()) { - LOG(ERROR) << "range sink underrun?"; + LOG(ERROR) << "Failed to fully write target blocks (range sink underrun): Missing " + << writer.AvailableSpace() << " bytes"; + failure_type = kPatchApplicationFailure; + return -1; } } else { LOG(INFO) << "skipping " << blocks << " blocks already patched to " << tgt.blocks() << " [" @@ -1445,14 +1461,9 @@ static int PerformCommandErase(CommandParameters& params) { LOG(INFO) << " erasing " << tgt.blocks() << " blocks"; for (const auto& [begin, end] : tgt) { - uint64_t blocks[2]; - // offset in bytes - blocks[0] = begin * static_cast<uint64_t>(BLOCKSIZE); - // length in bytes - blocks[1] = (end - begin) * static_cast<uint64_t>(BLOCKSIZE); - - if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) { - PLOG(ERROR) << "BLKDISCARD ioctl failed"; + off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE; + size_t size = (end - begin) * BLOCKSIZE; + if (!discard_blocks(params.fd, offset, size, true /* force */)) { return -1; } } @@ -1514,7 +1525,7 @@ static int PerformCommandComputeHashTree(CommandParameters& params) { // Starts the hash_tree computation. HashTreeBuilder builder(BLOCKSIZE, hash_function); - if (!builder.Initialize(source_ranges.blocks() * BLOCKSIZE, salt)) { + if (!builder.Initialize(static_cast<int64_t>(source_ranges.blocks()) * BLOCKSIZE, salt)) { LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString() << ", salt " << salt_hex; return -1; @@ -1570,6 +1581,43 @@ using CommandFunction = std::function<int(CommandParameters&)>; using CommandMap = std::unordered_map<Command::Type, CommandFunction>; +static bool Sha1DevicePath(const std::string& path, uint8_t digest[SHA_DIGEST_LENGTH]) { + auto device_name = android::base::Basename(path); + auto dm_target_name_path = "/sys/block/" + device_name + "/dm/name"; + + struct stat sb; + if (stat(dm_target_name_path.c_str(), &sb) == 0) { + // This is a device mapper target. Use partition name as part of the hash instead. Do not + // include extents as part of the hash, because the size of a partition may be shrunk after + // the patches are applied. + std::string dm_target_name; + if (!android::base::ReadFileToString(dm_target_name_path, &dm_target_name)) { + PLOG(ERROR) << "Cannot read " << dm_target_name_path; + return false; + } + SHA1(reinterpret_cast<const uint8_t*>(dm_target_name.data()), dm_target_name.size(), digest); + return true; + } + + if (errno != ENOENT) { + // This is a device mapper target, but its name cannot be retrieved. + PLOG(ERROR) << "Cannot get dm target name for " << path; + return false; + } + + // This doesn't appear to be a device mapper target, but if its name starts with dm-, something + // else might have gone wrong. + if (android::base::StartsWith(device_name, "dm-")) { + LOG(WARNING) << "Device " << path << " starts with dm- but is not mapped by device-mapper."; + } + + // Stash directory should be different for each partition to avoid conflicts when updating + // multiple partitions at the same time, so we use the hash of the block device name as the base + // directory. + SHA1(reinterpret_cast<const uint8_t*>(path.data()), path.size(), digest); + return true; +} + static Value* PerformBlockImageUpdate(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv, const CommandMap& command_map, bool dryrun) { @@ -1654,12 +1702,10 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, return StringValue(""); } - // Stash directory should be different for each partition to avoid conflicts when updating - // multiple partitions at the same time, so we use the hash of the block device name as the base - // directory. uint8_t digest[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast<const uint8_t*>(blockdev_filename->data.data()), - blockdev_filename->data.size(), digest); + if (!Sha1DevicePath(blockdev_filename->data, digest)) { + return StringValue(""); + } params.stashbase = print_sha1(digest); // Possibly do return early on retry, by checking the marker. If the update on this partition has @@ -1869,8 +1915,10 @@ pbiudone: const char* partition = strrchr(blockdev_filename->data.c_str(), '/'); if (partition != nullptr && *(partition + 1) != 0) { - fprintf(cmd_pipe, "log bytes_written_%s: %zu\n", partition + 1, params.written * BLOCKSIZE); - fprintf(cmd_pipe, "log bytes_stashed_%s: %zu\n", partition + 1, params.stashed * BLOCKSIZE); + fprintf(cmd_pipe, "log bytes_written_%s: %" PRIu64 "\n", partition + 1, + static_cast<uint64_t>(params.written) * BLOCKSIZE); + fprintf(cmd_pipe, "log bytes_stashed_%s: %" PRIu64 "\n", partition + 1, + static_cast<uint64_t>(params.stashed) * BLOCKSIZE); fflush(cmd_pipe); } // Delete stash only after successfully completing the update, as it may contain blocks needed @@ -1881,7 +1929,7 @@ pbiudone: // Create a marker on /cache partition, which allows skipping the update on this partition on // retry. The marker will be removed once booting into normal boot, or before starting next // fresh install. - if (!SetPartitionUpdatedMarker(updated_marker)) { + if (!SetUpdatedMarker(updated_marker)) { LOG(WARNING) << "Failed to set updated marker; continuing"; } } diff --git a/updater/dynamic_partitions.cpp b/updater/dynamic_partitions.cpp new file mode 100644 index 000000000..b50dd75f9 --- /dev/null +++ b/updater/dynamic_partitions.cpp @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "updater/dynamic_partitions.h" + +#include <sys/stat.h> +#include <sys/types.h> + +#include <algorithm> +#include <chrono> +#include <iterator> +#include <memory> +#include <optional> +#include <string> +#include <type_traits> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/parseint.h> +#include <android-base/strings.h> +#include <fs_mgr.h> +#include <fs_mgr_dm_linear.h> +#include <libdm/dm.h> +#include <liblp/builder.h> + +#include "edify/expr.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "private/utils.h" + +using android::base::ParseUint; +using android::dm::DeviceMapper; +using android::dm::DmDeviceState; +using android::fs_mgr::CreateLogicalPartition; +using android::fs_mgr::DestroyLogicalPartition; +using android::fs_mgr::LpMetadata; +using android::fs_mgr::MetadataBuilder; +using android::fs_mgr::Partition; +using android::fs_mgr::PartitionOpener; + +static constexpr std::chrono::milliseconds kMapTimeout{ 1000 }; +static constexpr char kMetadataUpdatedMarker[] = "/dynamic_partition_metadata.UPDATED"; + +static std::string GetSuperDevice() { + return "/dev/block/by-name/" + fs_mgr_get_super_partition_name(); +} + +static std::vector<std::string> ReadStringArgs(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv, + const std::vector<std::string>& arg_names) { + if (argv.size() != arg_names.size()) { + ErrorAbort(state, kArgsParsingFailure, "%s expects %zu arguments, got %zu", name, + arg_names.size(), argv.size()); + return {}; + } + + std::vector<std::unique_ptr<Value>> args; + if (!ReadValueArgs(state, argv, &args)) { + return {}; + } + + CHECK_EQ(args.size(), arg_names.size()); + + for (size_t i = 0; i < arg_names.size(); ++i) { + if (args[i]->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "%s argument to %s must be string", + arg_names[i].c_str(), name); + return {}; + } + } + + std::vector<std::string> ret; + std::transform(args.begin(), args.end(), std::back_inserter(ret), + [](const auto& arg) { return arg->data; }); + return ret; +} + +static bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) { + auto state = DeviceMapper::Instance().GetState(partition_name); + if (state == DmDeviceState::INVALID) { + return true; + } + if (state == DmDeviceState::ACTIVE) { + return DestroyLogicalPartition(partition_name, kMapTimeout); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast<std::underlying_type_t<DmDeviceState>>(state); + return false; +} + +static bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) { + auto state = DeviceMapper::Instance().GetState(partition_name); + if (state == DmDeviceState::INVALID) { + return CreateLogicalPartition(GetSuperDevice(), 0 /* metadata slot */, partition_name, + true /* force writable */, kMapTimeout, path); + } + + if (state == DmDeviceState::ACTIVE) { + return DeviceMapper::Instance().GetDmDevicePathByName(partition_name, path); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast<std::underlying_type_t<DmDeviceState>>(state); + return false; +} + +Value* UnmapPartitionFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + return UnmapPartitionOnDeviceMapper(args[0]) ? StringValue("t") : StringValue(""); +} + +Value* MapPartitionFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + std::string path; + bool result = MapPartitionOnDeviceMapper(args[0], &path); + return result ? StringValue(path) : StringValue(""); +} + +namespace { // Ops + +struct OpParameters { + std::vector<std::string> tokens; + MetadataBuilder* builder; + + bool ExpectArgSize(size_t size) const { + CHECK(!tokens.empty()); + auto actual = tokens.size() - 1; + if (actual != size) { + LOG(ERROR) << "Op " << op() << " expects " << size << " args, got " << actual; + return false; + } + return true; + } + const std::string& op() const { + CHECK(!tokens.empty()); + return tokens[0]; + } + const std::string& arg(size_t pos) const { + CHECK_LE(pos + 1, tokens.size()); + return tokens[pos + 1]; + } + std::optional<uint64_t> uint_arg(size_t pos, const std::string& name) const { + auto str = arg(pos); + uint64_t ret; + if (!ParseUint(str, &ret)) { + LOG(ERROR) << "Op " << op() << " expects uint64 for argument " << name << ", got " << str; + return std::nullopt; + } + return ret; + } +}; + +using OpFunction = std::function<bool(const OpParameters&)>; +using OpMap = std::map<std::string, OpFunction>; + +bool PerformOpResize(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + auto size = params.uint_arg(1, "size"); + if (!size.has_value()) return false; + + auto partition = params.builder->FindPartition(partition_name); + if (partition == nullptr) { + LOG(ERROR) << "Failed to find partition " << partition_name + << " in dynamic partition metadata."; + return false; + } + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before resizing."; + return false; + } + if (!params.builder->ResizePartition(partition, size.value())) { + LOG(ERROR) << "Failed to resize partition " << partition_name << " to size " << *size << "."; + return false; + } + return true; +} + +bool PerformOpRemove(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& partition_name = params.arg(0); + + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before removing."; + return false; + } + params.builder->RemovePartition(partition_name); + return true; +} + +bool PerformOpAdd(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + const auto& group_name = params.arg(1); + + if (params.builder->AddPartition(partition_name, group_name, LP_PARTITION_ATTR_READONLY) == + nullptr) { + LOG(ERROR) << "Failed to add partition " << partition_name << " to group " << group_name << "."; + return false; + } + return true; +} + +bool PerformOpMove(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + const auto& new_group = params.arg(1); + + auto partition = params.builder->FindPartition(partition_name); + if (partition == nullptr) { + LOG(ERROR) << "Cannot move partition " << partition_name << " to group " << new_group + << " because it is not found."; + return false; + } + + auto old_group = partition->group_name(); + if (old_group != new_group) { + if (!params.builder->ChangePartitionGroup(partition, new_group)) { + LOG(ERROR) << "Cannot move partition " << partition_name << " from group " << old_group + << " to group " << new_group << "."; + return false; + } + } + return true; +} + +bool PerformOpAddGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name = params.arg(0); + auto maximum_size = params.uint_arg(1, "maximum_size"); + if (!maximum_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name); + if (group != nullptr) { + LOG(ERROR) << "Cannot add group " << group_name << " because it already exists."; + return false; + } + + if (maximum_size.value() == 0) { + LOG(WARNING) << "Adding group " << group_name << " with no size limits."; + } + + if (!params.builder->AddGroup(group_name, maximum_size.value())) { + LOG(ERROR) << "Failed to add group " << group_name << " with maximum size " + << maximum_size.value() << "."; + return false; + } + return true; +} + +bool PerformOpResizeGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name = params.arg(0); + auto new_size = params.uint_arg(1, "maximum_size"); + if (!new_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name); + if (group == nullptr) { + LOG(ERROR) << "Cannot resize group " << group_name << " because it is not found."; + return false; + } + + auto old_size = group->maximum_size(); + if (old_size != new_size.value()) { + if (!params.builder->ChangeGroupSize(group_name, new_size.value())) { + LOG(ERROR) << "Cannot resize group " << group_name << " from " << old_size << " to " + << new_size.value() << "."; + return false; + } + } + return true; +} + +std::vector<std::string> ListPartitionNamesInGroup(MetadataBuilder* builder, + const std::string& group_name) { + auto partitions = builder->ListPartitionsInGroup(group_name); + std::vector<std::string> partition_names; + std::transform(partitions.begin(), partitions.end(), std::back_inserter(partition_names), + [](Partition* partition) { return partition->name(); }); + return partition_names; +} + +bool PerformOpRemoveGroup(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& group_name = params.arg(0); + + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name); + if (!partition_names.empty()) { + LOG(ERROR) << "Cannot remove group " << group_name << " because it still contains partitions [" + << android::base::Join(partition_names, ", ") << "]"; + return false; + } + params.builder->RemoveGroupAndPartitions(group_name); + return true; +} + +bool PerformOpRemoveAllGroups(const OpParameters& params) { + if (!params.ExpectArgSize(0)) return false; + + auto group_names = params.builder->ListGroups(); + for (const auto& group_name : group_names) { + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name); + for (const auto& partition_name : partition_names) { + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before removing group " << group_name + << "."; + return false; + } + } + params.builder->RemoveGroupAndPartitions(group_name); + } + return true; +} + +} // namespace + +Value* UpdateDynamicPartitionsFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + if (argv.size() != 1) { + ErrorAbort(state, kArgsParsingFailure, "%s expects 1 arguments, got %zu", name, argv.size()); + return StringValue(""); + } + std::vector<std::unique_ptr<Value>> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + const std::unique_ptr<Value>& op_list_value = args[0]; + if (op_list_value->type != Value::Type::BLOB) { + ErrorAbort(state, kArgsParsingFailure, "op_list argument to %s must be blob", name); + return StringValue(""); + } + + std::string updated_marker = Paths::Get().stash_directory_base() + kMetadataUpdatedMarker; + if (state->is_retry) { + struct stat sb; + int result = stat(updated_marker.c_str(), &sb); + if (result == 0) { + LOG(INFO) << "Skipping already updated dynamic partition metadata based on marker"; + return StringValue("t"); + } + } else { + // Delete the obsolete marker if any. + std::string err; + if (!android::base::RemoveFileIfExists(updated_marker, &err)) { + LOG(ERROR) << "Failed to remove dynamic partition metadata updated marker " << updated_marker + << ": " << err; + return StringValue(""); + } + } + + auto super_device = GetSuperDevice(); + auto builder = MetadataBuilder::New(PartitionOpener(), super_device, 0); + if (builder == nullptr) { + LOG(ERROR) << "Failed to load dynamic partition metadata."; + return StringValue(""); + } + + static const OpMap op_map{ + // clang-format off + {"resize", PerformOpResize}, + {"remove", PerformOpRemove}, + {"add", PerformOpAdd}, + {"move", PerformOpMove}, + {"add_group", PerformOpAddGroup}, + {"resize_group", PerformOpResizeGroup}, + {"remove_group", PerformOpRemoveGroup}, + {"remove_all_groups", PerformOpRemoveAllGroups}, + // clang-format on + }; + + std::vector<std::string> lines = android::base::Split(op_list_value->data, "\n"); + for (const auto& line : lines) { + auto comment_idx = line.find('#'); + auto op_and_args = comment_idx == std::string::npos ? line : line.substr(0, comment_idx); + op_and_args = android::base::Trim(op_and_args); + if (op_and_args.empty()) continue; + + auto tokens = android::base::Split(op_and_args, " "); + const auto& op = tokens[0]; + auto it = op_map.find(op); + if (it == op_map.end()) { + LOG(ERROR) << "Unknown operation in op_list: " << op; + return StringValue(""); + } + OpParameters params; + params.tokens = tokens; + params.builder = builder.get(); + if (!it->second(params)) { + return StringValue(""); + } + } + + auto metadata = builder->Export(); + if (metadata == nullptr) { + LOG(ERROR) << "Failed to export metadata."; + return StringValue(""); + } + + if (!UpdatePartitionTable(super_device, *metadata, 0)) { + LOG(ERROR) << "Failed to write metadata."; + return StringValue(""); + } + + if (!SetUpdatedMarker(updated_marker)) { + LOG(ERROR) << "Failed to set metadata updated marker."; + return StringValue(""); + } + + return StringValue("t"); +} + +void RegisterDynamicPartitionsFunctions() { + RegisterFunction("unmap_partition", UnmapPartitionFn); + RegisterFunction("map_partition", MapPartitionFn); + RegisterFunction("update_dynamic_partitions", UpdateDynamicPartitionsFn); +} diff --git a/updater/include/private/utils.h b/updater/include/private/utils.h new file mode 100644 index 000000000..33cf6155d --- /dev/null +++ b/updater/include/private/utils.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +bool SetUpdatedMarker(const std::string& marker); diff --git a/updater/include/updater/dynamic_partitions.h b/updater/include/updater/dynamic_partitions.h new file mode 100644 index 000000000..31cf859c6 --- /dev/null +++ b/updater/include/updater/dynamic_partitions.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +void RegisterDynamicPartitionsFunctions(); diff --git a/updater/install.cpp b/updater/install.cpp index deb7a2b02..20a204a83 100644 --- a/updater/install.cpp +++ b/updater/install.cpp @@ -393,17 +393,20 @@ Value* UnmountFn(const char* name, State* state, const std::vector<std::unique_p return StringValue(mount_point); } -static int exec_cmd(const char* path, char* const argv[]) { +static int exec_cmd(const std::vector<std::string>& args) { + CHECK(!args.empty()); + auto argv = StringVectorToNullTerminatedArray(args); + pid_t child; if ((child = vfork()) == 0) { - execv(path, argv); + execv(argv[0], argv.data()); _exit(EXIT_FAILURE); } int status; waitpid(child, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - LOG(ERROR) << path << " failed with status " << WEXITSTATUS(status); + LOG(ERROR) << args[0] << " failed with status " << WEXITSTATUS(status); } return WEXITSTATUS(status); } @@ -453,66 +456,53 @@ Value* FormatFn(const char* name, State* state, const std::vector<std::unique_pt } if (fs_type == "ext4") { - const char* mke2fs_argv[] = { "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", - location.c_str(), nullptr, nullptr }; - std::string size_str; + std::vector<std::string> mke2fs_args = { + "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", location + }; if (size != 0) { - size_str = std::to_string(size / 4096LL); - mke2fs_argv[6] = size_str.c_str(); + mke2fs_args.push_back(std::to_string(size / 4096LL)); } - int status = exec_cmd(mke2fs_argv[0], const_cast<char**>(mke2fs_argv)); - if (status != 0) { + if (auto status = exec_cmd(mke2fs_args); status != 0) { LOG(ERROR) << name << ": mke2fs failed (" << status << ") on " << location; return StringValue(""); } - const char* e2fsdroid_argv[] = { "/system/bin/e2fsdroid", "-e", "-a", mount_point.c_str(), - location.c_str(), nullptr }; - status = exec_cmd(e2fsdroid_argv[0], const_cast<char**>(e2fsdroid_argv)); - if (status != 0) { + if (auto status = exec_cmd({ "/system/bin/e2fsdroid", "-e", "-a", mount_point, location }); + status != 0) { LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location; return StringValue(""); } return StringValue(location); - } else if (fs_type == "f2fs") { + } + + if (fs_type == "f2fs") { if (size < 0) { LOG(ERROR) << name << ": fs_size can't be negative for f2fs: " << fs_size; return StringValue(""); } - std::string num_sectors = std::to_string(size / 512); - - const char* f2fs_path = "/sbin/mkfs.f2fs"; - const char* f2fs_argv[] = { "mkfs.f2fs", - "-d1", - "-f", - "-O", "encrypt", - "-O", "quota", - "-O", "verity", - "-w", "512", - location.c_str(), - (size < 512) ? nullptr : num_sectors.c_str(), - nullptr }; - int status = exec_cmd(f2fs_path, const_cast<char**>(f2fs_argv)); - if (status != 0) { - LOG(ERROR) << name << ": mkfs.f2fs failed (" << status << ") on " << location; + std::vector<std::string> f2fs_args = { + "/system/bin/make_f2fs", "-g", "android", "-w", "512", location + }; + if (size >= 512) { + f2fs_args.push_back(std::to_string(size / 512)); + } + if (auto status = exec_cmd(f2fs_args); status != 0) { + LOG(ERROR) << name << ": make_f2fs failed (" << status << ") on " << location; return StringValue(""); } - const char* sload_argv[] = { "/sbin/sload.f2fs", "-t", mount_point.c_str(), location.c_str(), - nullptr }; - status = exec_cmd(sload_argv[0], const_cast<char**>(sload_argv)); - if (status != 0) { - LOG(ERROR) << name << ": sload.f2fs failed (" << status << ") on " << location; + if (auto status = exec_cmd({ "/system/bin/sload_f2fs", "-t", mount_point, location }); + status != 0) { + LOG(ERROR) << name << ": sload_f2fs failed (" << status << ") on " << location; return StringValue(""); } return StringValue(location); - } else { - LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \"" - << partition_type << "\""; } + LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \"" + << partition_type << "\""; return nullptr; } @@ -679,17 +669,12 @@ Value* RunProgramFn(const char* name, State* state, const std::vector<std::uniqu return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); } - char* args2[argv.size() + 1]; - for (size_t i = 0; i < argv.size(); i++) { - args2[i] = &args[i][0]; - } - args2[argv.size()] = nullptr; - - LOG(INFO) << "about to run program [" << args2[0] << "] with " << argv.size() << " args"; + auto exec_args = StringVectorToNullTerminatedArray(args); + LOG(INFO) << "about to run program [" << exec_args[0] << "] with " << argv.size() << " args"; pid_t child = fork(); if (child == 0) { - execv(args2[0], args2); + execv(exec_args[0], exec_args.data()); PLOG(ERROR) << "run_program: execv failed"; _exit(EXIT_FAILURE); } @@ -913,20 +898,12 @@ Value* Tune2FsFn(const char* name, State* state, const std::vector<std::unique_p return ErrorAbort(state, kArgsParsingFailure, "%s() could not read args", name); } - char* args2[argv.size() + 1]; - // Tune2fs expects the program name as its args[0] - args2[0] = const_cast<char*>(name); - if (args2[0] == nullptr) { - return nullptr; - } - for (size_t i = 0; i < argv.size(); ++i) { - args2[i + 1] = &args[i][0]; - } + // tune2fs expects the program name as its first arg. + args.insert(args.begin(), "tune2fs"); + auto tune2fs_args = StringVectorToNullTerminatedArray(args); - // tune2fs changes the file system parameters on an ext2 file system; it - // returns 0 on success. - int result = tune2fs_main(argv.size() + 1, args2); - if (result != 0) { + // tune2fs changes the filesystem parameters on an ext2 filesystem; it returns 0 on success. + if (auto result = tune2fs_main(tune2fs_args.size() - 1, tune2fs_args.data()); result != 0) { return ErrorAbort(state, kTune2FsFailure, "%s() returned error code %d", name, result); } return StringValue("t"); diff --git a/updater/updater.cpp b/updater/updater.cpp index e87c57a5c..7b5a3f938 100644 --- a/updater/updater.cpp +++ b/updater/updater.cpp @@ -35,6 +35,7 @@ #include "otautil/error_code.h" #include "otautil/sysutil.h" #include "updater/blockimg.h" +#include "updater/dynamic_partitions.h" #include "updater/install.h" // Generated by the makefile, this function defines the @@ -125,6 +126,7 @@ int main(int argc, char** argv) { RegisterBuiltins(); RegisterInstallFunctions(); RegisterBlockImageFunctions(); + RegisterDynamicPartitionsFunctions(); RegisterDeviceExtensions(); // Parse the script. diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml index 18d8425e1..0a2511617 100644 --- a/updater_sample/AndroidManifest.xml +++ b/updater_sample/AndroidManifest.xml @@ -33,7 +33,7 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name=".services.PrepareStreamingService"/> + <service android:name=".services.PrepareUpdateService"/> </application> </manifest> diff --git a/updater_sample/README.md b/updater_sample/README.md index f9c3fb8ec..2070ebc21 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -191,7 +191,7 @@ privileged system app, so it's granted the required permissions to access </privapp-permissions> ``` to `frameworks/base/data/etc/privapp-permissions-platform.xml` -5. Build sample app `mmma -j bootable/recovery/updater_sample`. +5. Build sample app `make -j SystemUpdaterSample`. 6. Build Android `make -j` 7. [Flash the device](https://source.android.com/setup/build/running) 8. Add update config files; look above at `## Update Config file`; @@ -225,7 +225,9 @@ privileged system app, so it's granted the required permissions to access ## Running tests -1. Build `mmma bootable/recovery/updater_sample/` +The commands are expected to be run from `$ANDROID_BUILD_TOP`. + +1. Build `make -j SystemUpdaterSample` and `make -j SystemUpdaterSampleTests`. 2. Install app `adb install $OUT/system/priv-app/SystemUpdaterSample/SystemUpdaterSample.apk` 3. Install tests @@ -235,8 +237,8 @@ privileged system app, so it's granted the required permissions to access 5. Run a test file ``` adb shell am instrument \ - -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \ - -c com.example.android.systemupdatersample.util.PayloadSpecsTest + -w -e class com.example.android.systemupdatersample.UpdateManagerTest#applyUpdate_appliesPayloadToUpdateEngine \ + com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner ``` diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java index 12a8f3f5f..c02e60846 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -17,19 +17,18 @@ package com.example.android.systemupdatersample; import android.content.Context; +import android.os.Handler; import android.os.UpdateEngine; import android.os.UpdateEngineCallback; import android.util.Log; -import com.example.android.systemupdatersample.services.PrepareStreamingService; -import com.example.android.systemupdatersample.util.PayloadSpecs; +import com.example.android.systemupdatersample.services.PrepareUpdateService; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineProperties; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.AtomicDouble; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -50,11 +49,10 @@ public class UpdateManager { private static final String TAG = "UpdateManager"; /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ - private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; private final UpdateEngine mUpdateEngine; - private final PayloadSpecs mPayloadSpecs; private AtomicInteger mUpdateEngineStatus = new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); @@ -84,9 +82,15 @@ public class UpdateManager { private final UpdateManager.UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); - public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) { + private final Handler mHandler; + + /** + * @param updateEngine UpdateEngine instance. + * @param handler Handler for {@link PrepareUpdateService} intent service. + */ + public UpdateManager(UpdateEngine updateEngine, Handler handler) { this.mUpdateEngine = updateEngine; - this.mPayloadSpecs = payloadSpecs; + this.mHandler = handler; } /** @@ -293,45 +297,17 @@ public class UpdateManager { mManualSwitchSlotRequired.set(false); } - if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { - applyAbNonStreamingUpdate(config); - } else { - applyAbStreamingUpdate(context, config); - } - } - - private void applyAbNonStreamingUpdate(UpdateConfig config) - throws UpdaterState.InvalidTransitionException { - UpdateData.Builder builder = UpdateData.builder() - .setExtraProperties(prepareExtraProperties(config)); - - try { - builder.setPayload(mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile())); - } catch (IOException e) { - Log.e(TAG, "Error creating payload spec", e); - setUpdaterState(UpdaterState.ERROR); - return; - } - updateEngineApplyPayload(builder.build()); - } - - private void applyAbStreamingUpdate(Context context, UpdateConfig config) { - UpdateData.Builder builder = UpdateData.builder() - .setExtraProperties(prepareExtraProperties(config)); - - Log.d(TAG, "Starting PrepareStreamingService"); - PrepareStreamingService.startService(context, config, (code, payloadSpec) -> { - if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { - builder.setPayload(payloadSpec); - builder.addExtraProperty("USER_AGENT=" + HTTP_USER_AGENT); - config.getAbConfig() - .getAuthorization() - .ifPresent(s -> builder.addExtraProperty("AUTHORIZATION=" + s)); - updateEngineApplyPayload(builder.build()); - } else { - Log.e(TAG, "PrepareStreamingService failed, result code is " + code); + Log.d(TAG, "Starting PrepareUpdateService"); + PrepareUpdateService.startService(context, config, mHandler, (code, payloadSpec) -> { + if (code != PrepareUpdateService.RESULT_CODE_SUCCESS) { + Log.e(TAG, "PrepareUpdateService failed, result code is " + code); setUpdaterStateSilent(UpdaterState.ERROR); + return; } + updateEngineApplyPayload(UpdateData.builder() + .setExtraProperties(prepareExtraProperties(config)) + .setPayload(payloadSpec) + .build()); }); } @@ -343,6 +319,12 @@ public class UpdateManager { // User will enable it manually by clicking "Switch Slot" button on the screen. extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT); } + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_STREAMING) { + extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); + config.getAbConfig() + .getAuthorization() + .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); + } return extraProperties; } @@ -497,14 +479,14 @@ public class UpdateManager { * system/update_engine/binder_service_android.cc in * function BinderUpdateEngineAndroidService::bind). * - * @param status one of {@link UpdateEngine.UpdateStatusConstants}. + * @param status one of {@link UpdateEngine.UpdateStatusConstants}. * @param progress a number from 0.0 to 1.0. */ private void onStatusUpdate(int status, float progress) { Log.d(TAG, String.format( - "onStatusUpdate invoked, status=%s, progress=%.2f", - status, - progress)); + "onStatusUpdate invoked, status=%s, progress=%.2f", + status, + progress)); int previousStatus = mUpdateEngineStatus.get(); mUpdateEngineStatus.set(status); @@ -555,7 +537,6 @@ public class UpdateManager { } /** - * * Contains update data - PayloadSpec and extra properties list. * * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}. diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java index 931404857..29eb13da7 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java +++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.RecoverySystem; import android.os.ResultReceiver; +import android.os.UpdateEngine; import android.util.Log; import com.example.android.systemupdatersample.PayloadSpec; @@ -41,7 +42,9 @@ import com.google.common.collect.ImmutableSet; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Optional; /** @@ -49,10 +52,10 @@ import java.util.Optional; * without downloading the whole package. And it constructs {@link PayloadSpec}. * All this work required to install streaming A/B updates. * - * PrepareStreamingService runs on it's own thread. It will notify activity + * PrepareUpdateService runs on it's own thread. It will notify activity * using interface {@link UpdateResultCallback} when update is ready to install. */ -public class PrepareStreamingService extends IntentService { +public class PrepareUpdateService extends IntentService { /** * UpdateResultCallback result codes. @@ -61,62 +64,63 @@ public class PrepareStreamingService extends IntentService { public static final int RESULT_CODE_ERROR = 1; /** - * This interface is used to send results from {@link PrepareStreamingService} to + * Extra params that will be sent to IntentService. + */ + public static final String EXTRA_PARAM_CONFIG = "config"; + public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver"; + + /** + * This interface is used to send results from {@link PrepareUpdateService} to * {@code MainActivity}. */ public interface UpdateResultCallback { - /** * Invoked when files are downloaded and payload spec is constructed. * - * @param resultCode result code, values are defined in {@link PrepareStreamingService} + * @param resultCode result code, values are defined in {@link PrepareUpdateService} * @param payloadSpec prepared payload spec for streaming update */ void onReceiveResult(int resultCode, PayloadSpec payloadSpec); } /** - * Starts PrepareStreamingService. + * Starts PrepareUpdateService. * - * @param context application context - * @param config update config + * @param context application context + * @param config update config * @param resultCallback callback that will be called when the update is ready to be installed */ public static void startService(Context context, UpdateConfig config, + Handler handler, UpdateResultCallback resultCallback) { - Log.d(TAG, "Starting PrepareStreamingService"); - ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback); - Intent intent = new Intent(context, PrepareStreamingService.class); + Log.d(TAG, "Starting PrepareUpdateService"); + ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback); + Intent intent = new Intent(context, PrepareUpdateService.class); intent.putExtra(EXTRA_PARAM_CONFIG, config); intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver); context.startService(intent); } - public PrepareStreamingService() { + public PrepareUpdateService() { super(TAG); } - private static final String TAG = "PrepareStreamingService"; - - /** - * Extra params that will be sent from Activity to IntentService. - */ - private static final String EXTRA_PARAM_CONFIG = "config"; - private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver"; + private static final String TAG = "PrepareUpdateService"; /** * The files that should be downloaded before streaming. */ private static final ImmutableSet<String> PRE_STREAMING_FILES_SET = ImmutableSet.of( - PackageFiles.CARE_MAP_FILE_NAME, - PackageFiles.COMPATIBILITY_ZIP_FILE_NAME, - PackageFiles.METADATA_FILE_NAME, - PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME + PackageFiles.CARE_MAP_FILE_NAME, + PackageFiles.COMPATIBILITY_ZIP_FILE_NAME, + PackageFiles.METADATA_FILE_NAME, + PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME ); private final PayloadSpecs mPayloadSpecs = new PayloadSpecs(); + private final UpdateEngine mUpdateEngine = new UpdateEngine(); @Override protected void onHandleIntent(Intent intent) { @@ -142,6 +146,17 @@ public class PrepareStreamingService extends IntentService { private PayloadSpec execute(UpdateConfig config) throws IOException, PreparationFailedException { + if (config.getAbConfig().getVerifyPayloadMetadata()) { + Log.i(TAG, "Verifying payload metadata with UpdateEngine."); + if (!verifyPayloadMetadata(config)) { + throw new PreparationFailedException("Payload metadata is not compatible"); + } + } + + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { + return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } + downloadPreStreamingFiles(config, OTA_PACKAGE_DIR); Optional<UpdateConfig.PackageFile> payloadBinary = @@ -173,9 +188,52 @@ public class PrepareStreamingService extends IntentService { } /** + * Downloads only payload_metadata.bin and verifies with + * {@link UpdateEngine#verifyPayloadMetadata}. + * Returns {@code true} if the payload is verified or the result is unknown because of + * exception from UpdateEngine. + * By downloading only small portion of the package, it allows to verify if UpdateEngine + * will install the update. + */ + private boolean verifyPayloadMetadata(UpdateConfig config) { + Optional<UpdateConfig.PackageFile> metadataPackageFile = + Arrays.stream(config.getAbConfig().getPropertyFiles()) + .filter(p -> p.getFilename().equals( + PackageFiles.PAYLOAD_METADATA_FILE_NAME)) + .findFirst(); + if (!metadataPackageFile.isPresent()) { + Log.w(TAG, String.format("ab_config.property_files doesn't contain %s", + PackageFiles.PAYLOAD_METADATA_FILE_NAME)); + return true; + } + Path metadataPath = Paths.get(OTA_PACKAGE_DIR, PackageFiles.PAYLOAD_METADATA_FILE_NAME); + try { + Files.deleteIfExists(metadataPath); + FileDownloader d = new FileDownloader( + config.getUrl(), + metadataPackageFile.get().getOffset(), + metadataPackageFile.get().getSize(), + metadataPath.toFile()); + d.download(); + } catch (IOException e) { + Log.w(TAG, String.format("Downloading %s from %s failed", + PackageFiles.PAYLOAD_METADATA_FILE_NAME, + config.getUrl()), e); + return true; + } + try { + return mUpdateEngine.verifyPayloadMetadata(metadataPath.toAbsolutePath().toString()); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine#verifyPayloadMetadata failed", e); + return true; + } + } + + /** * Downloads files defined in {@link UpdateConfig#getAbConfig()} * and exists in {@code PRE_STREAMING_FILES_SET}, and put them * in directory {@code dir}. + * * @throws IOException when can't download a file */ private void downloadPreStreamingFiles(UpdateConfig config, String dir) @@ -212,7 +270,7 @@ public class PrepareStreamingService extends IntentService { } /** - * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec} + * Used by {@link PrepareUpdateService} to pass {@link PayloadSpec} * to {@link UpdateResultCallback#onReceiveResult}. */ private static class CallbackResultReceiver extends ResultReceiver { diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java index fc9fddd70..6d1e4c35a 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -21,6 +21,7 @@ import android.app.AlertDialog; import android.graphics.Color; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.UpdateEngine; import android.util.Log; import android.view.View; @@ -34,7 +35,6 @@ import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.UpdateConfig; import com.example.android.systemupdatersample.UpdateManager; import com.example.android.systemupdatersample.UpdaterState; -import com.example.android.systemupdatersample.util.PayloadSpecs; import com.example.android.systemupdatersample.util.UpdateConfigs; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineStatuses; @@ -67,7 +67,7 @@ public class MainActivity extends Activity { private List<UpdateConfig> mConfigs; private final UpdateManager mUpdateManager = - new UpdateManager(new UpdateEngine(), new PayloadSpecs()); + new UpdateManager(new UpdateEngine(), new Handler()); @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java index ddd0919b8..0f9083d27 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java @@ -30,7 +30,7 @@ import java.net.URLConnection; * Downloads chunk of a file from given url using {@code offset} and {@code size}, * and saves to a given location. * - * In real-life application this helper class should download from HTTP Server, + * In a real-life application this helper class should download from HTTP Server, * but in this sample app it will only download from a local file. */ public final class FileDownloader { diff --git a/updater_sample/tests/Android.bp b/updater_sample/tests/Android.bp index c2783ef88..7867770a0 100644 --- a/updater_sample/tests/Android.bp +++ b/updater_sample/tests/Android.bp @@ -28,7 +28,7 @@ android_test { "guava", ], - instrumentation_for: "com.example.android.systemupdatersample", + instrumentation_for: "SystemUpdaterSample", optimize: { enabled: false, diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java index e05ad290c..5ad16d477 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java @@ -18,20 +18,25 @@ package com.example.android.systemupdatersample; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; import android.os.UpdateEngine; import android.os.UpdateEngineCallback; import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import com.example.android.systemupdatersample.services.PrepareUpdateService; import com.example.android.systemupdatersample.tests.R; -import com.example.android.systemupdatersample.util.PayloadSpecs; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; @@ -43,7 +48,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.io.File; import java.io.IOException; import java.io.InputStreamReader; @@ -60,49 +64,39 @@ public class UpdateManagerTest { @Mock private UpdateEngine mUpdateEngine; @Mock - private PayloadSpecs mPayloadSpecs; + private Context mMockContext; private UpdateManager mSubject; - private Context mContext; - private UpdateConfig mNonStreamingUpdate003; + private Context mTestContext; + private UpdateConfig mStreamingUpdate002; @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getContext(); - mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs); - mNonStreamingUpdate003 = - UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream)); + mTestContext = InstrumentationRegistry.getContext(); + mSubject = new UpdateManager(mUpdateEngine, null); + mStreamingUpdate002 = + UpdateConfig.fromJson(readResource(R.raw.update_config_002_stream)); } @Test public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception { - PayloadSpec payload = buildMockPayloadSpec(); - when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload); - when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { - // When UpdateManager is bound to update_engine, it passes - // UpdateEngineCallback as a callback to update_engine. - UpdateEngineCallback callback = answer.getArgument(0); - callback.onStatusUpdate( - UpdateEngine.UpdateStatusConstants.IDLE, - /*engineProgress*/ 0.0f); - return null; - }); - - mSubject.bind(); - mSubject.applyUpdate(null, mNonStreamingUpdate003); + mockContextStartServiceAnswer(buildMockPayloadSpec()); + mSubject.applyUpdate(mMockContext, mStreamingUpdate002); verify(mUpdateEngine).applyPayload( "file://blah", 120, 340, - new String[] { - "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false + new String[]{ + "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false + "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT }); } @Test - public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Exception { - PayloadSpec payload = buildMockPayloadSpec(); - when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload); + @UiThreadTest + public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Throwable { + mockContextStartServiceAnswer(buildMockPayloadSpec()); + // UpdateEngine always returns IDLE status. when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { // When UpdateManager is bound to update_engine, it passes // UpdateEngineCallback as a callback to update_engine. @@ -114,21 +108,36 @@ public class UpdateManagerTest { }); mSubject.bind(); - mSubject.applyUpdate(null, mNonStreamingUpdate003); + mSubject.applyUpdate(mMockContext, mStreamingUpdate002); mSubject.unbind(); mSubject.bind(); // re-bind - now it should re-apply last update assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING); - // it should be called 2 times verify(mUpdateEngine, times(2)).applyPayload( "file://blah", 120, 340, - new String[] { - "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false + new String[]{ + "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false + "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT }); } + private void mockContextStartServiceAnswer(PayloadSpec payloadSpec) { + doAnswer(args -> { + Intent intent = args.getArgument(0); + ResultReceiver resultReceiver = intent.getParcelableExtra( + PrepareUpdateService.EXTRA_PARAM_RESULT_RECEIVER); + Bundle b = new Bundle(); + b.putSerializable( + /* PrepareUpdateService.CallbackResultReceiver.BUNDLE_PARAM_PAYLOAD_SPEC */ + "payload-spec", + payloadSpec); + resultReceiver.send(PrepareUpdateService.RESULT_CODE_SUCCESS, b); + return null; + }).when(mMockContext).startService(any(Intent.class)); + } + private PayloadSpec buildMockPayloadSpec() { PayloadSpec payload = mock(PayloadSpec.class); when(payload.getUrl()).thenReturn("file://blah"); @@ -140,7 +149,7 @@ public class UpdateManagerTest { private String readResource(int id) throws IOException { return CharStreams.toString(new InputStreamReader( - mContext.getResources().openRawResource(id))); + mTestContext.getResources().openRawResource(id))); } } diff --git a/verifier.cpp b/verifier.cpp index 283e04300..b6c3895ce 100644 --- a/verifier.cpp +++ b/verifier.cpp @@ -27,9 +27,14 @@ #include <vector> #include <android-base/logging.h> +#include <openssl/bio.h> #include <openssl/bn.h> #include <openssl/ecdsa.h> +#include <openssl/evp.h> #include <openssl/obj_mac.h> +#include <openssl/pem.h> +#include <openssl/rsa.h> +#include <ziparchive/zip_archive.h> #include "asn1_decoder.h" #include "otautil/print_sha1.h" @@ -112,19 +117,9 @@ static bool read_pkcs7(const uint8_t* pkcs7_der, size_t pkcs7_der_len, return true; } -/* - * Looks for an RSA signature embedded in the .ZIP file comment given the path to the zip. Verifies - * that it matches one of the given public keys. A callback function can be optionally provided for - * posting the progress. - * - * Returns VERIFY_SUCCESS or VERIFY_FAILURE (if any error is encountered or no key matches the - * signature). - */ -int verify_file(const unsigned char* addr, size_t length, const std::vector<Certificate>& keys, - const std::function<void(float)>& set_progress) { - if (set_progress) { - set_progress(0.0); - } +int verify_file(VerifierInterface* package, const std::vector<Certificate>& keys) { + CHECK(package); + package->SetProgress(0.0); // An archive with a whole-file signature will end in six bytes: // @@ -135,13 +130,18 @@ int verify_file(const unsigned char* addr, size_t length, const std::vector<Cert // the whole comment. #define FOOTER_SIZE 6 + uint64_t length = package->GetPackageSize(); if (length < FOOTER_SIZE) { LOG(ERROR) << "not big enough to contain footer"; return VERIFY_FAILURE; } - const unsigned char* footer = addr + length - FOOTER_SIZE; + uint8_t footer[FOOTER_SIZE]; + if (!package->ReadFullyAtOffset(footer, FOOTER_SIZE, length - FOOTER_SIZE)) { + LOG(ERROR) << "Failed to read footer"; + return VERIFY_FAILURE; + } if (footer[2] != 0xff || footer[3] != 0xff) { LOG(ERROR) << "footer is wrong"; @@ -177,9 +177,13 @@ int verify_file(const unsigned char* addr, size_t length, const std::vector<Cert // Determine how much of the file is covered by the signature. This is everything except the // signature data and length, which includes all of the EOCD except for the comment length field // (2 bytes) and the comment data. - size_t signed_len = length - eocd_size + EOCD_HEADER_SIZE - 2; + uint64_t signed_len = length - eocd_size + EOCD_HEADER_SIZE - 2; - const unsigned char* eocd = addr + length - eocd_size; + uint8_t eocd[eocd_size]; + if (!package->ReadFullyAtOffset(eocd, eocd_size, length - eocd_size)) { + LOG(ERROR) << "Failed to read EOCD of " << eocd_size << " bytes"; + return VERIFY_FAILURE; + } // If this is really is the EOCD record, it will begin with the magic number $50 $4b $05 $06. if (eocd[0] != 0x50 || eocd[1] != 0x4b || eocd[2] != 0x05 || eocd[3] != 0x06) { @@ -211,24 +215,29 @@ int verify_file(const unsigned char* addr, size_t length, const std::vector<Cert SHA1_Init(&sha1_ctx); SHA256_Init(&sha256_ctx); + std::vector<HasherUpdateCallback> hashers; + if (need_sha1) { + hashers.emplace_back( + std::bind(&SHA1_Update, &sha1_ctx, std::placeholders::_1, std::placeholders::_2)); + } + if (need_sha256) { + hashers.emplace_back( + std::bind(&SHA256_Update, &sha256_ctx, std::placeholders::_1, std::placeholders::_2)); + } + double frac = -1.0; - size_t so_far = 0; + uint64_t so_far = 0; while (so_far < signed_len) { - // On a Nexus 5X, experiment showed 16MiB beat 1MiB by 6% faster for a - // 1196MiB full OTA and 60% for an 89MiB incremental OTA. - // http://b/28135231. - size_t size = std::min(signed_len - so_far, 16 * MiB); - - if (need_sha1) SHA1_Update(&sha1_ctx, addr + so_far, size); - if (need_sha256) SHA256_Update(&sha256_ctx, addr + so_far, size); - so_far += size; - - if (set_progress) { - double f = so_far / (double)signed_len; - if (f > frac + 0.02 || size == so_far) { - set_progress(f); - frac = f; - } + // On a Nexus 5X, experiment showed 16MiB beat 1MiB by 6% faster for a 1196MiB full OTA and + // 60% for an 89MiB incremental OTA. http://b/28135231. + uint64_t read_size = std::min<uint64_t>(signed_len - so_far, 16 * MiB); + package->UpdateHashAtOffset(hashers, so_far, read_size); + so_far += read_size; + + double f = so_far / static_cast<double>(signed_len); + if (f > frac + 0.02 || read_size == so_far) { + package->SetProgress(f); + frac = f; } } @@ -303,251 +312,160 @@ int verify_file(const unsigned char* addr, size_t length, const std::vector<Cert return VERIFY_FAILURE; } -std::unique_ptr<RSA, RSADeleter> parse_rsa_key(FILE* file, uint32_t exponent) { - // Read key length in words and n0inv. n0inv is a precomputed montgomery - // parameter derived from the modulus and can be used to speed up - // verification. n0inv is 32 bits wide here, assuming the verification logic - // uses 32 bit arithmetic. However, BoringSSL may use a word size of 64 bits - // internally, in which case we don't have a valid n0inv. Thus, we just - // ignore the montgomery parameters and have BoringSSL recompute them - // internally. If/When the speedup from using the montgomery parameters - // becomes relevant, we can add more sophisticated code here to obtain a - // 64-bit n0inv and initialize the montgomery parameters in the key object. - uint32_t key_len_words = 0; - uint32_t n0inv = 0; - if (fscanf(file, " %i , 0x%x", &key_len_words, &n0inv) != 2) { - return nullptr; - } +static std::vector<Certificate> IterateZipEntriesAndSearchForKeys(const ZipArchiveHandle& handle) { + void* cookie; + ZipString suffix("x509.pem"); + int32_t iter_status = StartIteration(handle, &cookie, nullptr, &suffix); + if (iter_status != 0) { + LOG(ERROR) << "Failed to iterate over entries in the certificate zipfile: " + << ErrorCodeString(iter_status); + return {}; + } - if (key_len_words > 8192 / 32) { - LOG(ERROR) << "key length (" << key_len_words << ") too large"; - return nullptr; + std::vector<Certificate> result; + + ZipString name; + ZipEntry entry; + while ((iter_status = Next(cookie, &entry, &name)) == 0) { + std::vector<uint8_t> pem_content(entry.uncompressed_length); + if (int32_t extract_status = + ExtractToMemory(handle, &entry, pem_content.data(), pem_content.size()); + extract_status != 0) { + LOG(ERROR) << "Failed to extract " << std::string(name.name, name.name + name.name_length); + return {}; } - // Read the modulus. - std::unique_ptr<uint32_t[]> modulus(new uint32_t[key_len_words]); - if (fscanf(file, " , { %u", &modulus[0]) != 1) { - return nullptr; - } - for (uint32_t i = 1; i < key_len_words; ++i) { - if (fscanf(file, " , %u", &modulus[i]) != 1) { - return nullptr; - } + Certificate cert(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); + // Aborts the parsing if we fail to load one of the key file. + if (!LoadCertificateFromBuffer(pem_content, &cert)) { + LOG(ERROR) << "Failed to load keys from " + << std::string(name.name, name.name + name.name_length); + return {}; } - // Cconvert from little-endian array of little-endian words to big-endian - // byte array suitable as input for BN_bin2bn. - std::reverse((uint8_t*)modulus.get(), - (uint8_t*)(modulus.get() + key_len_words)); - - // The next sequence of values is the montgomery parameter R^2. Since we - // generally don't have a valid |n0inv|, we ignore this (see comment above). - uint32_t rr_value; - if (fscanf(file, " } , { %u", &rr_value) != 1) { - return nullptr; - } - for (uint32_t i = 1; i < key_len_words; ++i) { - if (fscanf(file, " , %u", &rr_value) != 1) { - return nullptr; - } - } - if (fscanf(file, " } } ") != 0) { - return nullptr; - } + result.emplace_back(std::move(cert)); + } - // Initialize the key. - std::unique_ptr<RSA, RSADeleter> key(RSA_new()); - if (!key) { - return nullptr; - } + if (iter_status != -1) { + LOG(ERROR) << "Error while iterating over zip entries: " << ErrorCodeString(iter_status); + return {}; + } - key->n = BN_bin2bn((uint8_t*)modulus.get(), - key_len_words * sizeof(uint32_t), NULL); - if (!key->n) { - return nullptr; - } + return result; +} - key->e = BN_new(); - if (!key->e || !BN_set_word(key->e, exponent)) { - return nullptr; - } +std::vector<Certificate> LoadKeysFromZipfile(const std::string& zip_name) { + ZipArchiveHandle handle; + if (int32_t open_status = OpenArchive(zip_name.c_str(), &handle); open_status != 0) { + LOG(ERROR) << "Failed to open " << zip_name << ": " << ErrorCodeString(open_status); + return {}; + } - return key; + std::vector<Certificate> result = IterateZipEntriesAndSearchForKeys(handle); + CloseArchive(handle); + return result; } -struct BNDeleter { - void operator()(BIGNUM* bn) const { - BN_free(bn); +bool CheckRSAKey(const std::unique_ptr<RSA, RSADeleter>& rsa) { + if (!rsa) { + return false; } -}; -std::unique_ptr<EC_KEY, ECKEYDeleter> parse_ec_key(FILE* file) { - uint32_t key_len_bytes = 0; - if (fscanf(file, " %i", &key_len_bytes) != 1) { - return nullptr; - } - - std::unique_ptr<EC_GROUP, void (*)(EC_GROUP*)> group( - EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1), EC_GROUP_free); - if (!group) { - return nullptr; - } + const BIGNUM* out_n; + const BIGNUM* out_e; + RSA_get0_key(rsa.get(), &out_n, &out_e, nullptr /* private exponent */); + auto modulus_bits = BN_num_bits(out_n); + if (modulus_bits != 2048) { + LOG(ERROR) << "Modulus should be 2048 bits long, actual: " << modulus_bits; + return false; + } - // Verify that |key_len| matches the group order. - if (key_len_bytes != BN_num_bytes(EC_GROUP_get0_order(group.get()))) { - return nullptr; - } + BN_ULONG exponent = BN_get_word(out_e); + if (exponent != 3 && exponent != 65537) { + LOG(ERROR) << "Public exponent should be 3 or 65537, actual: " << exponent; + return false; + } - // Read the public key coordinates. Note that the byte order in the file is - // little-endian, so we convert to big-endian here. - std::unique_ptr<uint8_t[]> bytes(new uint8_t[key_len_bytes]); - std::unique_ptr<BIGNUM, BNDeleter> point[2]; - for (int i = 0; i < 2; ++i) { - unsigned int byte = 0; - if (fscanf(file, " , { %u", &byte) != 1) { - return nullptr; - } - bytes[key_len_bytes - 1] = byte; - - for (size_t i = 1; i < key_len_bytes; ++i) { - if (fscanf(file, " , %u", &byte) != 1) { - return nullptr; - } - bytes[key_len_bytes - i - 1] = byte; - } - - point[i].reset(BN_bin2bn(bytes.get(), key_len_bytes, nullptr)); - if (!point[i]) { - return nullptr; - } - - if (fscanf(file, " }") != 0) { - return nullptr; - } - } + return true; +} - if (fscanf(file, " } ") != 0) { - return nullptr; - } +bool CheckECKey(const std::unique_ptr<EC_KEY, ECKEYDeleter>& ec_key) { + if (!ec_key) { + return false; + } - // Create and initialize the key. - std::unique_ptr<EC_KEY, ECKEYDeleter> key(EC_KEY_new()); - if (!key || !EC_KEY_set_group(key.get(), group.get()) || - !EC_KEY_set_public_key_affine_coordinates(key.get(), point[0].get(), - point[1].get())) { - return nullptr; - } + const EC_GROUP* ec_group = EC_KEY_get0_group(ec_key.get()); + if (!ec_group) { + LOG(ERROR) << "Failed to get the ec_group from the ec_key"; + return false; + } + auto degree = EC_GROUP_get_degree(ec_group); + if (degree != 256) { + LOG(ERROR) << "Field size of the ec key should be 256 bits long, actual: " << degree; + return false; + } - return key; + return true; } -// Reads a file containing one or more public keys as produced by -// DumpPublicKey: this is an RSAPublicKey struct as it would appear -// as a C source literal, eg: -// -// "{64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}" -// -// For key versions newer than the original 2048-bit e=3 keys -// supported by Android, the string is preceded by a version -// identifier, eg: -// -// "v2 {64,0xc926ad21,{1795090719,...,-695002876},{-857949815,...,1175080310}}" -// -// (Note that the braces and commas in this example are actual -// characters the parser expects to find in the file; the ellipses -// indicate more numbers omitted from this example.) -// -// The file may contain multiple keys in this format, separated by -// commas. The last key must not be followed by a comma. -// -// A Certificate is a pair of an RSAPublicKey and a particular hash -// (we support SHA-1 and SHA-256; we store the hash length to signify -// which is being used). The hash used is implied by the version number. -// -// 1: 2048-bit RSA key with e=3 and SHA-1 hash -// 2: 2048-bit RSA key with e=65537 and SHA-1 hash -// 3: 2048-bit RSA key with e=3 and SHA-256 hash -// 4: 2048-bit RSA key with e=65537 and SHA-256 hash -// 5: 256-bit EC key using the NIST P-256 curve parameters and SHA-256 hash -// -// Returns true on success, and appends the found keys (at least one) to certs. -// Otherwise returns false if the file failed to parse, or if it contains zero -// keys. The contents in certs would be unspecified on failure. -bool load_keys(const char* filename, std::vector<Certificate>& certs) { - std::unique_ptr<FILE, decltype(&fclose)> f(fopen(filename, "re"), fclose); - if (!f) { - PLOG(ERROR) << "error opening " << filename; +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; } - while (true) { - certs.emplace_back(0, Certificate::KEY_TYPE_RSA, nullptr, nullptr); - Certificate& cert = certs.back(); - uint32_t exponent = 0; - - char start_char; - if (fscanf(f.get(), " %c", &start_char) != 1) return false; - if (start_char == '{') { - // a version 1 key has no version specifier. - cert.key_type = Certificate::KEY_TYPE_RSA; - exponent = 3; - cert.hash_len = SHA_DIGEST_LENGTH; - } else if (start_char == 'v') { - int version; - if (fscanf(f.get(), "%d {", &version) != 1) return false; - switch (version) { - case 2: - cert.key_type = Certificate::KEY_TYPE_RSA; - exponent = 65537; - cert.hash_len = SHA_DIGEST_LENGTH; - break; - case 3: - cert.key_type = Certificate::KEY_TYPE_RSA; - exponent = 3; - cert.hash_len = SHA256_DIGEST_LENGTH; - break; - case 4: - cert.key_type = Certificate::KEY_TYPE_RSA; - exponent = 65537; - cert.hash_len = SHA256_DIGEST_LENGTH; - break; - case 5: - cert.key_type = Certificate::KEY_TYPE_EC; - cert.hash_len = SHA256_DIGEST_LENGTH; - break; - default: - return false; - } - } + 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; + } - if (cert.key_type == Certificate::KEY_TYPE_RSA) { - cert.rsa = parse_rsa_key(f.get(), exponent); - if (!cert.rsa) { - 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; + } - LOG(INFO) << "read key e=" << exponent << " hash=" << cert.hash_len; - } else if (cert.key_type == Certificate::KEY_TYPE_EC) { - cert.ec = parse_ec_key(f.get()); - if (!cert.ec) { - return false; - } - } else { - LOG(ERROR) << "Unknown key type " << cert.key_type; + int key_type = EVP_PKEY_id(public_key.get()); + if (key_type == EVP_PKEY_RSA) { + cert->key_type = Certificate::KEY_TYPE_RSA; + cert->ec.reset(); + cert->rsa.reset(EVP_PKEY_get1_RSA(public_key.get())); + if (!cert->rsa || !CheckRSAKey(cert->rsa)) { + LOG(ERROR) << "Failed to validate the rsa key info from public key"; return false; } - - // if the line ends in a comma, this file has more keys. - int ch = fgetc(f.get()); - if (ch == ',') { - // more keys to come. - continue; - } else if (ch == EOF) { - break; - } else { - LOG(ERROR) << "unexpected character between keys"; + } else if (key_type == EVP_PKEY_EC) { + cert->key_type = Certificate::KEY_TYPE_EC; + cert->rsa.reset(); + cert->ec.reset(EVP_PKEY_get1_EC_KEY(public_key.get())); + if (!cert->ec || !CheckECKey(cert->ec)) { + LOG(ERROR) << "Failed to validate the ec key info from the public key"; return false; } + } else { + LOG(ERROR) << "Unrecognized public key type " << OBJ_nid2ln(key_type); + return false; } + return true; } diff --git a/verifier.h b/verifier.h index 6fa8f2b0a..b80096d41 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> @@ -25,6 +27,8 @@ #include <openssl/rsa.h> #include <openssl/sha.h> +using HasherUpdateCallback = std::function<void(const uint8_t* addr, uint64_t size)>; + struct RSADeleter { void operator()(RSA* rsa) const { RSA_free(rsa); @@ -59,16 +63,42 @@ struct Certificate { std::unique_ptr<EC_KEY, ECKEYDeleter> ec; }; -/* - * 'addr' and 'length' define an update package file that has been loaded (or mmap'ed, or - * whatever) into memory. Verifies that the file is signed and the signature matches one of the - * given keys. It optionally accepts a callback function for posting the progress to. Returns one - * of the constants of VERIFY_SUCCESS and VERIFY_FAILURE. - */ -int verify_file(const unsigned char* addr, size_t length, const std::vector<Certificate>& keys, - const std::function<void(float)>& set_progress = nullptr); +class VerifierInterface { + public: + virtual ~VerifierInterface() = default; + + // Returns the package size in bytes. + virtual uint64_t GetPackageSize() const = 0; + + // Reads |byte_count| data starting from |offset|, and puts the result in |buffer|. + virtual bool ReadFullyAtOffset(uint8_t* buffer, uint64_t byte_count, uint64_t offset) = 0; + + // Updates the hash contexts for |length| bytes data starting from |start|. + virtual bool UpdateHashAtOffset(const std::vector<HasherUpdateCallback>& hashers, uint64_t start, + uint64_t length) = 0; + + // Updates the progress in fraction during package verification. + virtual void SetProgress(float progress) = 0; +}; + +// Looks for an RSA signature embedded in the .ZIP file comment given the path to the zip. +// Verifies that it matches one of the given public keys. Returns VERIFY_SUCCESS or +// VERIFY_FAILURE (if any error is encountered or no key matches the signature). +int verify_file(VerifierInterface* package, const std::vector<Certificate>& keys); + +// Checks that the RSA key has a modulus of 2048 bits long, and public exponent is 3 or 65537. +bool CheckRSAKey(const std::unique_ptr<RSA, RSADeleter>& rsa); + +// Checks that the field size of the curve for the EC key is 256 bits. +bool CheckECKey(const std::unique_ptr<EC_KEY, ECKEYDeleter>& ec_key); + +// Parses a PEM-encoded x509 certificate from the given buffer and saves it into |cert|. Returns +// false if there is a parsing failure or the signature's encryption algorithm is not supported. +bool LoadCertificateFromBuffer(const std::vector<uint8_t>& pem_content, Certificate* cert); -bool load_keys(const char* filename, std::vector<Certificate>& certs); +// Iterates over the zip entries with the suffix "x509.pem" and returns a list of recognized +// certificates. Returns an empty list if we fail to parse any of the entries. +std::vector<Certificate> LoadKeysFromZipfile(const std::string& zip_name); #define VERIFY_SUCCESS 0 #define VERIFY_FAILURE 1 @@ -34,13 +34,13 @@ int VrRecoveryUI::ScreenHeight() const { return gr_fb_height(); } -void VrRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, +void VrRecoveryUI::DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const { gr_blit(surface, sx, sy, w, h, dx + stereo_offset_, dy); gr_blit(surface, sx, sy, w, h, dx - stereo_offset_ + ScreenWidth(), dy); } -void VrRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const { +void VrRecoveryUI::DrawTextIcon(int x, int y, const GRSurface* surface) const { gr_texticon(x + stereo_offset_, y, surface); gr_texticon(x - stereo_offset_ + ScreenWidth(), y, surface); } @@ -33,11 +33,12 @@ class VrRecoveryUI : public ScreenRecoveryUI { int ScreenWidth() const override; int ScreenHeight() const override; - void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const override; + void DrawSurface(const GRSurface* surface, int sx, int sy, int w, int h, int dx, + int dy) const override; int DrawHorizontalRule(int y) const override; void DrawHighlightBar(int x, int y, int width, int height) const override; void DrawFill(int x, int y, int w, int h) const override; - void DrawTextIcon(int x, int y, GRSurface* surface) const override; + void DrawTextIcon(int x, int y, const GRSurface* surface) const override; int DrawTextLine(int x, int y, const std::string& line, bool bold) const override; }; diff --git a/wear_ui.cpp b/wear_ui.cpp index 3b057b761..6da84c924 100644 --- a/wear_ui.cpp +++ b/wear_ui.cpp @@ -51,8 +51,8 @@ void WearRecoveryUI::draw_background_locked() { gr_color(0, 0, 0, 255); gr_fill(0, 0, gr_fb_width(), gr_fb_height()); - if (currentIcon != NONE) { - GRSurface* frame = GetCurrentFrame(); + if (current_icon_ != NONE) { + const auto& frame = GetCurrentFrame(); int frame_width = gr_get_width(frame); int frame_height = gr_get_height(frame); int frame_x = (gr_fb_width() - frame_width) / 2; @@ -60,7 +60,7 @@ void WearRecoveryUI::draw_background_locked() { gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y); // Draw recovery text on screen above progress bar. - GRSurface* text = GetCurrentText(); + const auto& text = GetCurrentText(); int text_x = (ScreenWidth() - gr_get_width(text)) / 2; int text_y = GetProgressBaseline() - gr_get_height(text) - 10; gr_color(255, 255, 255, 255); @@ -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 @@ -95,12 +95,14 @@ void WearRecoveryUI::update_progress_locked() { void WearRecoveryUI::SetStage(int /* current */, int /* max */) {} -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); +std::unique_ptr<Menu> WearRecoveryUI::CreateMenu(const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const { if (text_rows_ > 0 && text_cols_ > 0) { - menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1, - text_cols_ - 1, headers, items, initial_selection); - update_screen_locked(); + return std::make_unique<TextMenu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1, + text_cols_ - 1, text_headers, text_items, initial_selection, + char_height_, *this); } + + return nullptr; } @@ -36,8 +36,9 @@ class WearRecoveryUI : public ScreenRecoveryUI { // Recovery, build id and etc) and the bottom lines that may otherwise go out of the screen. const int menu_unusable_rows_; - void StartMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items, - size_t initial_selection) override; + std::unique_ptr<Menu> CreateMenu(const std::vector<std::string>& text_headers, + const std::vector<std::string>& text_items, + size_t initial_selection) const override; int GetProgressBaseline() const override; |