[automerger skipped] DO NOT MERGE: Initialize the ZipArchive to zero before parsing skipped: e830a51351 skipped: 244aba4412 skipped: 9216bccd16 skipped: 98acdb9fce skipped: bc42a6b0eb am: 35332d7c62 am: 6b13f89f15 am: 274945cb5b -s ours
am: 71b2447163
Change-Id: I4c58e2c29d3d791f91bfd1aee91b5571d8fe9de9
diff --git a/.clang-format b/.clang-format
index 0e0f4d1..4a3bd2f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,3 +1,33 @@
+# bootable/recovery project uses repohook to apply `clang-format` to the changed lines, with the
+# local style file in `.clang-format`. This will be triggered automatically with `repo upload`.
+# Alternatively, one can stage and format a change with `git clang-format` directly.
+#
+# $ git add <files>
+# $ git clang-format --style file
+#
+# Or to format a committed change.
+#
+# $ git clang-format --style file HEAD~1
+#
+# `--style file` will pick up the local style file in `.clang-format`. This can be configured as the
+# default behavior for bootable/recovery project.
+#
+# $ git config --local clangFormat.style file
+#
+# Note that `repo upload` calls the `clang-format` binary in Android repo (i.e.
+# `$ANDROID_BUILD_TOP/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format`), which might
+# give slightly different results from the one installed in host machine (e.g.
+# `/usr/bin/clang-format`). Specifying the file with `--binary` will ensure consistent results.
+#
+# $ git clang-format --binary \
+# /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+# Or to do one-time setup to make it default.
+#
+# $ git config --local clangFormat.binary \
+# /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+
BasedOnStyle: Google
AllowShortBlocksOnASingleLine: false
AllowShortFunctionsOnASingleLine: Empty
diff --git a/Android.bp b/Android.bp
index f8c6a4b..76e6985 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,8 +1,304 @@
-subdirs = [
- "applypatch",
- "bootloader_message",
- "edify",
- "otafault",
- "otautil",
- "uncrypt",
-]
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+ name: "recovery_defaults",
+
+ cflags: [
+ "-D_FILE_OFFSET_BITS=64",
+
+ // Must be the same as RECOVERY_API_VERSION.
+ "-DRECOVERY_API_VERSION=3",
+
+ "-Wall",
+ "-Werror",
+ ],
+
+ cpp_std: "c++17",
+}
+
+cc_library {
+ name: "librecovery_ui",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "device.cpp",
+ "screen_ui.cpp",
+ "ui.cpp",
+ "vr_ui.cpp",
+ "wear_ui.cpp"
+ ],
+
+ static_libs: [
+ "libminui",
+ "libotautil",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libpng",
+ "libz",
+ ],
+}
+
+// Generic device that uses ScreenRecoveryUI.
+cc_library_static {
+ name: "librecovery_ui_default",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "default_device.cpp",
+ ],
+}
+
+// The default wear device that uses WearRecoveryUI.
+cc_library_static {
+ name: "librecovery_ui_wear",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "wear_device.cpp",
+ ],
+}
+
+// The default VR device that uses VrRecoveryUI.
+cc_library_static {
+ name: "librecovery_ui_vr",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "vr_device.cpp",
+ ],
+}
+
+cc_library_static {
+ name: "librecovery_fastboot",
+ recovery_available: true,
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "fastboot/fastboot.cpp",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libbootloader_message",
+ "libcutils",
+ "liblog",
+ ],
+
+ static_libs: [
+ "librecovery_ui_default",
+ ],
+}
+
+cc_defaults {
+ name: "librecovery_defaults",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ shared_libs: [
+ "android.hardware.health@2.0",
+ "libbase",
+ "libbootloader_message",
+ "libcrypto",
+ "libcutils",
+ "libext4_utils",
+ "libfs_mgr",
+ "libfusesideload",
+ "libhidl-gen-utils",
+ "libhidlbase",
+ "libhidltransport",
+ "liblog",
+ "libpng",
+ "libselinux",
+ "libtinyxml2",
+ "libutils",
+ "libz",
+ "libziparchive",
+ ],
+
+ static_libs: [
+ "librecovery_fastboot",
+ "libminui",
+ "libverifier",
+ "libotautil",
+
+ // external dependencies
+ "libhealthhalutils",
+ "libvintf_recovery",
+ "libvintf",
+ ],
+}
+
+cc_library_static {
+ name: "librecovery",
+ recovery_available: true,
+
+ defaults: [
+ "librecovery_defaults",
+ ],
+
+ srcs: [
+ "adb_install.cpp",
+ "fsck_unshare_blocks.cpp",
+ "fuse_sdcard_provider.cpp",
+ "install.cpp",
+ "recovery.cpp",
+ "roots.cpp",
+ ],
+
+ include_dirs: [
+ "system/vold",
+ ],
+}
+
+cc_library_static {
+ name: "libverifier",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "asn1_decoder.cpp",
+ "verifier.cpp",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libcrypto",
+ "libcrypto_utils",
+ ],
+
+ static_libs: [
+ "libotautil",
+ ],
+}
+
+cc_binary {
+ name: "recovery",
+ recovery: true,
+
+ defaults: [
+ "librecovery_defaults",
+ ],
+
+ srcs: [
+ "logging.cpp",
+ "recovery_main.cpp",
+ ],
+
+ shared_libs: [
+ "libminadbd_services",
+ "librecovery_ui",
+ ],
+
+ static_libs: [
+ "librecovery",
+ "librecovery_ui_default",
+ ],
+
+ required: [
+ "e2fsdroid.recovery",
+ "librecovery_ui_ext",
+ "mke2fs.conf.recovery",
+ "mke2fs.recovery",
+ "recovery_deps",
+ ],
+}
+
+// The dynamic executable that runs after /data mounts.
+cc_binary {
+ name: "recovery-persist",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "logging.cpp",
+ "recovery-persist.cpp",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "liblog",
+ "libmetricslogger",
+ ],
+
+ static_libs: [
+ "libotautil",
+ ],
+
+ init_rc: [
+ "recovery-persist.rc",
+ ],
+}
+
+// The dynamic executable that runs at init.
+cc_binary {
+ name: "recovery-refresh",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ srcs: [
+ "logging.cpp",
+ "recovery-refresh.cpp",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "liblog",
+ ],
+
+ static_libs: [
+ "libotautil",
+ ],
+
+ init_rc: [
+ "recovery-refresh.rc",
+ ],
+}
+
+filegroup {
+ name: "res-testdata",
+
+ srcs: [
+ "res-*/images/*_text.png",
+ ],
+}
diff --git a/Android.mk b/Android.mk
index 7e0ad12..7be1230 100644
--- a/Android.mk
+++ b/Android.mk
@@ -14,267 +14,73 @@
LOCAL_PATH := $(call my-dir)
-# Needed by build/make/core/Makefile.
+# Needed by build/make/core/Makefile. Must be consistent with the value in Android.bp.
RECOVERY_API_VERSION := 3
RECOVERY_FSTAB_VERSION := 2
-# libfusesideload (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := fuse_sideload.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_CFLAGS += -D_XOPEN_SOURCE -D_GNU_SOURCE
-LOCAL_MODULE := libfusesideload
-LOCAL_STATIC_LIBRARIES := \
- libcrypto \
- libbase
-include $(BUILD_STATIC_LIBRARY)
+# TARGET_RECOVERY_UI_LIB should be one of librecovery_ui_{default,wear,vr} or a device-specific
+# module that defines make_device() and the exact RecoveryUI class for the target. It defaults to
+# librecovery_ui_default, which uses ScreenRecoveryUI.
+TARGET_RECOVERY_UI_LIB ?= librecovery_ui_default
-# libmounts (static library)
-# ===============================
+# librecovery_ui_ext (shared library)
+# ===================================
include $(CLEAR_VARS)
-LOCAL_SRC_FILES := mounts.cpp
-LOCAL_CFLAGS := \
- -Wall \
- -Werror
-LOCAL_MODULE := libmounts
-LOCAL_STATIC_LIBRARIES := libbase
-include $(BUILD_STATIC_LIBRARY)
-# librecovery (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
- install.cpp
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
+LOCAL_MODULE := librecovery_ui_ext
-ifeq ($(AB_OTA_UPDATER),true)
- LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
+# LOCAL_MODULE_PATH for shared libraries is unsupported in multiarch builds.
+LOCAL_MULTILIB := first
+
+ifeq ($(TARGET_IS_64_BIT),true)
+LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/system/lib64
+else
+LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/system/lib
endif
-LOCAL_MODULE := librecovery
-LOCAL_STATIC_LIBRARIES := \
- libminui \
- libotautil \
- libvintf_recovery \
- libcrypto_utils \
- libcrypto \
+LOCAL_WHOLE_STATIC_LIBRARIES := \
+ $(TARGET_RECOVERY_UI_LIB)
+
+LOCAL_SHARED_LIBRARIES := \
libbase \
- libziparchive \
+ liblog \
+ librecovery_ui.recovery
-include $(BUILD_STATIC_LIBRARY)
+include $(BUILD_SHARED_LIBRARY)
-# recovery (static executable)
-# ===============================
+# recovery_deps: A phony target that's depended on by `recovery`, which
+# builds additional modules conditionally based on Makefile variables.
+# ======================================================================
include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
- adb_install.cpp \
- device.cpp \
- fuse_sdcard_provider.cpp \
- recovery.cpp \
- roots.cpp \
- rotate_logs.cpp \
- screen_ui.cpp \
- ui.cpp \
- vr_ui.cpp \
- wear_ui.cpp \
-
-LOCAL_MODULE := recovery
-
-LOCAL_FORCE_STATIC_EXECUTABLE := true
-
-LOCAL_REQUIRED_MODULES := e2fsdroid_static mke2fs_static mke2fs.conf
+LOCAL_MODULE := recovery_deps
ifeq ($(TARGET_USERIMAGES_USE_F2FS),true)
ifeq ($(HOST_OS),linux)
-LOCAL_REQUIRED_MODULES += sload.f2fs mkfs.f2fs
+LOCAL_REQUIRED_MODULES += \
+ sload.f2fs \
+ mkfs.f2fs
endif
endif
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
-LOCAL_CFLAGS += -Wall -Werror
-
-ifneq ($(TARGET_RECOVERY_UI_MARGIN_HEIGHT),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=$(TARGET_RECOVERY_UI_MARGIN_HEIGHT)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=0
+# 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
+endif
endif
-ifneq ($(TARGET_RECOVERY_UI_MARGIN_WIDTH),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_WIDTH=$(TARGET_RECOVERY_UI_MARGIN_WIDTH)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_WIDTH=0
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_TOUCH_LOW_THRESHOLD),)
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_LOW_THRESHOLD=$(TARGET_RECOVERY_UI_TOUCH_LOW_THRESHOLD)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_LOW_THRESHOLD=50
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_TOUCH_HIGH_THRESHOLD),)
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_HIGH_THRESHOLD=$(TARGET_RECOVERY_UI_TOUCH_HIGH_THRESHOLD)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_TOUCH_HIGH_THRESHOLD=90
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_PROGRESS_BAR_BASELINE),)
-LOCAL_CFLAGS += -DRECOVERY_UI_PROGRESS_BAR_BASELINE=$(TARGET_RECOVERY_UI_PROGRESS_BAR_BASELINE)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_PROGRESS_BAR_BASELINE=259
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_ANIMATION_FPS),)
-LOCAL_CFLAGS += -DRECOVERY_UI_ANIMATION_FPS=$(TARGET_RECOVERY_UI_ANIMATION_FPS)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_ANIMATION_FPS=30
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_MENU_UNUSABLE_ROWS),)
-LOCAL_CFLAGS += -DRECOVERY_UI_MENU_UNUSABLE_ROWS=$(TARGET_RECOVERY_UI_MENU_UNUSABLE_ROWS)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_MENU_UNUSABLE_ROWS=9
-endif
-
-ifneq ($(TARGET_RECOVERY_UI_VR_STEREO_OFFSET),)
-LOCAL_CFLAGS += -DRECOVERY_UI_VR_STEREO_OFFSET=$(TARGET_RECOVERY_UI_VR_STEREO_OFFSET)
-else
-LOCAL_CFLAGS += -DRECOVERY_UI_VR_STEREO_OFFSET=0
-endif
-
-LOCAL_C_INCLUDES += \
- system/vold \
-
-# Health HAL dependency
-LOCAL_STATIC_LIBRARIES := \
- android.hardware.health@2.0-impl \
- android.hardware.health@2.0 \
- android.hardware.health@1.0 \
- android.hardware.health@1.0-convert \
- libhealthstoragedefault \
- libhidltransport \
- libhidlbase \
- libhwbinder_noltopgo \
- libvndksupport \
- libbatterymonitor
-
-LOCAL_STATIC_LIBRARIES += \
- librecovery \
- libverifier \
- libbootloader_message \
- libfs_mgr \
- libext4_utils \
- libsparse \
- libziparchive \
- libotautil \
- libmounts \
- libminadbd \
- libasyncio \
- libfusesideload \
- libminui \
- libpng \
- libcrypto_utils \
- libcrypto \
- libvintf_recovery \
- libvintf \
- libhidl-gen-utils \
- libtinyxml2 \
- libbase \
- libutils \
- libcutils \
- liblog \
- libselinux \
- libz
-
-LOCAL_HAL_STATIC_LIBRARIES := libhealthd
-
-ifeq ($(AB_OTA_UPDATER),true)
- LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
-endif
-
-LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/sbin
-
-ifeq ($(TARGET_RECOVERY_UI_LIB),)
- LOCAL_SRC_FILES += default_device.cpp
-else
- LOCAL_STATIC_LIBRARIES += $(TARGET_RECOVERY_UI_LIB)
-endif
-
+# On A/B devices recovery-persist reads the recovery related file from the persist storage and
+# copies them into /data/misc/recovery. Then, for both A/B and non-A/B devices, recovery-persist
+# parses the last_install file and reports the embedded update metrics. Also, the last_install file
+# will be deteleted after the report.
+LOCAL_REQUIRED_MODULES += recovery-persist
ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),)
-LOCAL_REQUIRED_MODULES += recovery-persist recovery-refresh
+LOCAL_REQUIRED_MODULES += recovery-refresh
endif
-include $(BUILD_EXECUTABLE)
-
-# recovery-persist (system partition dynamic executable run after /data mounts)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
- recovery-persist.cpp \
- rotate_logs.cpp
-LOCAL_MODULE := recovery-persist
-LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_INIT_RC := recovery-persist.rc
-include $(BUILD_EXECUTABLE)
-
-# recovery-refresh (system partition dynamic executable run at init)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := \
- recovery-refresh.cpp \
- rotate_logs.cpp
-LOCAL_MODULE := recovery-refresh
-LOCAL_SHARED_LIBRARIES := liblog libbase
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_INIT_RC := recovery-refresh.rc
-include $(BUILD_EXECUTABLE)
-
-# libverifier (static library)
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_MODULE := libverifier
-LOCAL_SRC_FILES := \
- asn1_decoder.cpp \
- verifier.cpp
-LOCAL_STATIC_LIBRARIES := \
- libotautil \
- libcrypto_utils \
- libcrypto \
- libbase
-LOCAL_CFLAGS := -Wall -Werror
-include $(BUILD_STATIC_LIBRARY)
-
-# Wear default device
-# ===============================
-include $(CLEAR_VARS)
-LOCAL_SRC_FILES := wear_device.cpp
-LOCAL_CFLAGS := -Wall -Werror
-
-# Should match TARGET_RECOVERY_UI_LIB in BoardConfig.mk.
-LOCAL_MODULE := librecovery_ui_wear
-
-include $(BUILD_STATIC_LIBRARY)
-
-# vr headset default device
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := vr_device.cpp
-LOCAL_CFLAGS := -Wall -Werror
-
-# should match TARGET_RECOVERY_UI_LIB set in BoardConfig.mk
-LOCAL_MODULE := librecovery_ui_vr
-
-include $(BUILD_STATIC_LIBRARY)
+include $(BUILD_PHONY_PACKAGE)
include \
- $(LOCAL_PATH)/boot_control/Android.mk \
- $(LOCAL_PATH)/minadbd/Android.mk \
- $(LOCAL_PATH)/minui/Android.mk \
- $(LOCAL_PATH)/tests/Android.mk \
- $(LOCAL_PATH)/tools/Android.mk \
$(LOCAL_PATH)/updater/Android.mk \
- $(LOCAL_PATH)/update_verifier/Android.mk \
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index b5f5f03..1084291 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -4,3 +4,8 @@
[Builtin Hooks Options]
# Handle native codes only.
clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
+
+[Hook Scripts]
+checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
+ -fw updater_sample/
+
diff --git a/adb_install.cpp b/adb_install.cpp
index ac01306..438da9f 100644
--- a/adb_install.cpp
+++ b/adb_install.cpp
@@ -26,55 +26,23 @@
#include <sys/wait.h>
#include <unistd.h>
-#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/properties.h>
-#include <android-base/unique_fd.h>
#include "common.h"
#include "fuse_sideload.h"
#include "install.h"
#include "ui.h"
-static void set_usb_driver(bool enabled) {
- // USB configfs doesn't use /s/c/a/a/enable.
- if (android::base::GetBoolProperty("sys.usb.configfs", false)) {
- return;
- }
-
- static constexpr const char* USB_DRIVER_CONTROL = "/sys/class/android_usb/android0/enable";
- android::base::unique_fd fd(open(USB_DRIVER_CONTROL, O_WRONLY));
- if (fd == -1) {
- PLOG(ERROR) << "Failed to open driver control";
- return;
- }
- // Not using android::base::WriteStringToFile since that will open with O_CREAT and give EPERM
- // when USB_DRIVER_CONTROL doesn't exist. When it gives EPERM, we don't know whether that's due
- // to non-existent USB_DRIVER_CONTROL or indeed a permission issue.
- if (!android::base::WriteStringToFd(enabled ? "1" : "0", fd)) {
- PLOG(ERROR) << "Failed to set driver control";
- }
-}
-
-static void stop_adbd() {
- ui->Print("Stopping adbd...\n");
- android::base::SetProperty("ctl.stop", "adbd");
- set_usb_driver(false);
-}
-
-static void maybe_restart_adbd() {
- if (is_ro_debuggable()) {
- ui->Print("Restarting adbd...\n");
- set_usb_driver(true);
- android::base::SetProperty("ctl.start", "adbd");
- }
-}
-
-int apply_from_adb(bool* wipe_cache, const char* install_file) {
+int apply_from_adb(bool* wipe_cache) {
modified_flash = true;
-
- stop_adbd();
- set_usb_driver(true);
+ // Save the usb state to restore after the sideload operation.
+ std::string usb_state = android::base::GetProperty("sys.usb.state", "none");
+ // Clean up state and stop adbd.
+ if (usb_state != "none" && !SetUsbConfig("none")) {
+ LOG(ERROR) << "Failed to clear USB config";
+ return INSTALL_ERROR;
+ }
ui->Print(
"\n\nNow send the package you want to apply\n"
@@ -82,10 +50,15 @@
pid_t child;
if ((child = fork()) == 0) {
- execl("/sbin/recovery", "recovery", "--adbd", nullptr);
+ execl("/system/bin/recovery", "recovery", "--adbd", nullptr);
_exit(EXIT_FAILURE);
}
+ if (!SetUsbConfig("sideload")) {
+ LOG(ERROR) << "Failed to set usb config to sideload";
+ return INSTALL_ERROR;
+ }
+
// How long (in seconds) we wait for the host to start sending us a package, before timing out.
static constexpr int ADB_INSTALL_TIMEOUT = 300;
@@ -113,7 +86,7 @@
break;
}
}
- result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, install_file, false, 0);
+ result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0);
break;
}
@@ -136,8 +109,17 @@
}
}
- set_usb_driver(false);
- maybe_restart_adbd();
+ // Clean up before switching to the older state, for example setting the state
+ // to none sets sys/class/android_usb/android0/enable to 0.
+ if (!SetUsbConfig("none")) {
+ LOG(ERROR) << "Failed to clear USB config";
+ }
+
+ if (usb_state != "none") {
+ if (!SetUsbConfig(usb_state)) {
+ LOG(ERROR) << "Failed to set USB config to " << usb_state;
+ }
+ }
return result;
}
diff --git a/adb_install.h b/adb_install.h
index e654c89..cc3ca26 100644
--- a/adb_install.h
+++ b/adb_install.h
@@ -17,6 +17,6 @@
#ifndef _ADB_INSTALL_H
#define _ADB_INSTALL_H
-int apply_from_adb(bool* wipe_cache, const char* install_file);
+int apply_from_adb(bool* wipe_cache);
#endif
diff --git a/applypatch/Android.bp b/applypatch/Android.bp
index cb0b367..620ca6c 100644
--- a/applypatch/Android.bp
+++ b/applypatch/Android.bp
@@ -53,7 +53,6 @@
"libbz",
"libcrypto",
"libedify",
- "libotafault",
"libotautil",
"libz",
],
@@ -100,7 +99,6 @@
"libapplypatch_modes",
"libapplypatch",
"libedify",
- "libotafault",
"libotautil",
"libbspatch",
],
diff --git a/applypatch/applypatch.cpp b/applypatch/applypatch.cpp
index 7645a40..f9383dd 100644
--- a/applypatch/applypatch.cpp
+++ b/applypatch/applypatch.cpp
@@ -23,261 +23,167 @@
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
-#include <sys/statfs.h>
#include <sys/types.h>
#include <unistd.h>
+#include <algorithm>
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
+#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
#include <openssl/sha.h>
#include "edify/expr.h"
-#include "otafault/ota_io.h"
-#include "otautil/cache_location.h"
+#include "otautil/paths.h"
#include "otautil/print_sha1.h"
-static int LoadPartitionContents(const std::string& filename, FileContents* file);
-static size_t FileSink(const unsigned char* data, size_t len, int fd);
-static int GenerateTarget(const FileContents& source_file, const std::unique_ptr<Value>& patch,
- const std::string& target_filename,
- const uint8_t target_sha1[SHA_DIGEST_LENGTH], const Value* bonus_data);
+using namespace std::string_literals;
-// Read a file into memory; store the file contents and associated metadata in *file.
-// Return 0 on success.
-int LoadFileContents(const char* filename, FileContents* file) {
- // A special 'filename' beginning with "EMMC:" means to load the contents of a partition.
- if (strncmp(filename, "EMMC:", 5) == 0) {
- return LoadPartitionContents(filename, file);
+static bool GenerateTarget(const Partition& target, const FileContents& source_file,
+ const Value& patch, const Value* bonus_data);
+
+bool LoadFileContents(const std::string& filename, FileContents* file) {
+ // No longer allow loading contents from eMMC partitions.
+ if (android::base::StartsWith(filename, "EMMC:")) {
+ return false;
}
- struct stat sb;
- if (stat(filename, &sb) == -1) {
- printf("failed to stat \"%s\": %s\n", filename, strerror(errno));
- return -1;
+ std::string data;
+ if (!android::base::ReadFileToString(filename, &data)) {
+ PLOG(ERROR) << "Failed to read \"" << filename << "\"";
+ return false;
}
- std::vector<unsigned char> data(sb.st_size);
- unique_file f(ota_fopen(filename, "rb"));
- if (!f) {
- printf("failed to open \"%s\": %s\n", filename, strerror(errno));
- return -1;
- }
-
- size_t bytes_read = ota_fread(data.data(), 1, data.size(), f.get());
- if (bytes_read != data.size()) {
- printf("short read of \"%s\" (%zu bytes of %zu)\n", filename, bytes_read, data.size());
- return -1;
- }
- file->data = std::move(data);
+ file->data = std::vector<unsigned char>(data.begin(), data.end());
SHA1(file->data.data(), file->data.size(), file->sha1);
- return 0;
+ return true;
}
-// Load the contents of an EMMC partition into the provided
-// FileContents. filename should be a string of the form
-// "EMMC:<partition_device>:...". The smallest size_n bytes for
-// which that prefix of the partition contents has the corresponding
-// sha1 hash will be loaded. It is acceptable for a size value to be
-// repeated with different sha1s. Will return 0 on success.
-//
-// This complexity is needed because if an OTA installation is
-// interrupted, the partition might contain either the source or the
-// target data, which might be of different lengths. We need to know
-// the length in order to read from a partition (there is no
-// "end-of-file" marker), so the caller must specify the possible
-// lengths and the hash of the data, and we'll do the load expecting
-// to find one of those hashes.
-static int LoadPartitionContents(const std::string& filename, FileContents* file) {
- std::vector<std::string> pieces = android::base::Split(filename, ":");
- if (pieces.size() < 4 || pieces.size() % 2 != 0 || pieces[0] != "EMMC") {
- printf("LoadPartitionContents called with bad filename \"%s\"\n", filename.c_str());
- return -1;
+// Reads the contents of a Partition to the given FileContents buffer.
+static bool ReadPartitionToBuffer(const Partition& partition, FileContents* out,
+ bool check_backup) {
+ uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+ if (ParseSha1(partition.hash, expected_sha1) != 0) {
+ LOG(ERROR) << "Failed to parse target hash \"" << partition.hash << "\"";
+ return false;
}
- size_t pair_count = (pieces.size() - 2) / 2; // # of (size, sha1) pairs in filename
- std::vector<std::pair<size_t, std::string>> pairs;
- for (size_t i = 0; i < pair_count; ++i) {
- size_t size;
- if (!android::base::ParseUint(pieces[i * 2 + 2], &size) || size == 0) {
- printf("LoadPartitionContents called with bad size \"%s\"\n", pieces[i * 2 + 2].c_str());
- return -1;
- }
- pairs.push_back({ size, pieces[i * 2 + 3] });
- }
-
- // Sort the pairs array so that they are in order of increasing size.
- std::sort(pairs.begin(), pairs.end());
-
- const char* partition = pieces[1].c_str();
- unique_file dev(ota_fopen(partition, "rb"));
+ android::base::unique_fd dev(open(partition.name.c_str(), O_RDONLY));
if (!dev) {
- printf("failed to open emmc partition \"%s\": %s\n", partition, strerror(errno));
- return -1;
- }
-
- SHA_CTX sha_ctx;
- SHA1_Init(&sha_ctx);
-
- // Allocate enough memory to hold the largest size.
- std::vector<unsigned char> buffer(pairs[pair_count - 1].first);
- unsigned char* buffer_ptr = buffer.data();
- size_t buffer_size = 0; // # bytes read so far
- bool found = false;
-
- for (const auto& pair : pairs) {
- size_t current_size = pair.first;
- const std::string& current_sha1 = pair.second;
-
- // Read enough additional bytes to get us up to the next size. (Again,
- // we're trying the possibilities in order of increasing size).
- size_t next = current_size - buffer_size;
- if (next > 0) {
- size_t read = ota_fread(buffer_ptr, 1, next, dev.get());
- if (next != read) {
- printf("short read (%zu bytes of %zu) for partition \"%s\"\n", read, next, partition);
- return -1;
+ PLOG(ERROR) << "Failed to open eMMC partition \"" << partition << "\"";
+ } else {
+ std::vector<unsigned char> buffer(partition.size);
+ if (!android::base::ReadFully(dev, buffer.data(), buffer.size())) {
+ PLOG(ERROR) << "Failed to read " << buffer.size() << " bytes of data for partition "
+ << partition;
+ } else {
+ SHA1(buffer.data(), buffer.size(), out->sha1);
+ if (memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) {
+ out->data = std::move(buffer);
+ return true;
}
- SHA1_Update(&sha_ctx, buffer_ptr, read);
- buffer_size += read;
- buffer_ptr += read;
- }
-
- // Duplicate the SHA context and finalize the duplicate so we can
- // check it against this pair's expected hash.
- SHA_CTX temp_ctx;
- memcpy(&temp_ctx, &sha_ctx, sizeof(SHA_CTX));
- uint8_t sha_so_far[SHA_DIGEST_LENGTH];
- SHA1_Final(sha_so_far, &temp_ctx);
-
- uint8_t parsed_sha[SHA_DIGEST_LENGTH];
- if (ParseSha1(current_sha1.c_str(), parsed_sha) != 0) {
- printf("failed to parse SHA-1 %s in %s\n", current_sha1.c_str(), filename.c_str());
- return -1;
- }
-
- if (memcmp(sha_so_far, parsed_sha, SHA_DIGEST_LENGTH) == 0) {
- // We have a match. Stop reading the partition; we'll return the data we've read so far.
- printf("partition read matched size %zu SHA-1 %s\n", current_size, current_sha1.c_str());
- found = true;
- break;
}
}
- if (!found) {
- // Ran off the end of the list of (size, sha1) pairs without finding a match.
- printf("contents of partition \"%s\" didn't match %s\n", partition, filename.c_str());
- return -1;
+ if (!check_backup) {
+ LOG(ERROR) << "Partition contents don't have the expected checksum";
+ return false;
}
- SHA1_Final(file->sha1, &sha_ctx);
+ if (LoadFileContents(Paths::Get().cache_temp_source(), out) &&
+ memcmp(out->sha1, expected_sha1, SHA_DIGEST_LENGTH) == 0) {
+ return true;
+ }
- buffer.resize(buffer_size);
- file->data = std::move(buffer);
-
- return 0;
+ LOG(ERROR) << "Both of partition contents and backup don't have the expected checksum";
+ return false;
}
-// Save the contents of the given FileContents object under the given
-// filename. Return 0 on success.
-int SaveFileContents(const char* filename, const FileContents* file) {
- unique_fd fd(ota_open(filename, O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR));
+bool SaveFileContents(const std::string& filename, const FileContents* file) {
+ android::base::unique_fd fd(
+ open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC | O_SYNC, S_IRUSR | S_IWUSR));
if (fd == -1) {
- printf("failed to open \"%s\" for write: %s\n", filename, strerror(errno));
- return -1;
+ PLOG(ERROR) << "Failed to open \"" << filename << "\" for write";
+ return false;
}
- size_t bytes_written = FileSink(file->data.data(), file->data.size(), fd);
- if (bytes_written != file->data.size()) {
- printf("short write of \"%s\" (%zd bytes of %zu): %s\n", filename, bytes_written,
- file->data.size(), strerror(errno));
- return -1;
- }
- if (ota_fsync(fd) != 0) {
- printf("fsync of \"%s\" failed: %s\n", filename, strerror(errno));
- return -1;
- }
- if (ota_close(fd) != 0) {
- printf("close of \"%s\" failed: %s\n", filename, strerror(errno));
- return -1;
+ if (!android::base::WriteFully(fd, file->data.data(), file->data.size())) {
+ PLOG(ERROR) << "Failed to write " << file->data.size() << " bytes of data to " << filename;
+ return false;
}
- return 0;
+ if (fsync(fd) != 0) {
+ PLOG(ERROR) << "Failed to fsync \"" << filename << "\"";
+ return false;
+ }
+
+ if (close(fd.release()) != 0) {
+ PLOG(ERROR) << "Failed to close \"" << filename << "\"";
+ return false;
+ }
+
+ return true;
}
-// Write a memory buffer to 'target' partition, a string of the form
-// "EMMC:<partition_device>[:...]". The target name
-// might contain multiple colons, but WriteToPartition() only uses the first
-// two and ignores the rest. Return 0 on success.
-int WriteToPartition(const unsigned char* data, size_t len, const std::string& target) {
- std::vector<std::string> pieces = android::base::Split(target, ":");
- if (pieces.size() < 2 || pieces[0] != "EMMC") {
- printf("WriteToPartition called with bad target (%s)\n", target.c_str());
- return -1;
- }
-
- const char* partition = pieces[1].c_str();
- unique_fd fd(ota_open(partition, O_RDWR));
- if (fd == -1) {
- printf("failed to open %s: %s\n", partition, strerror(errno));
- return -1;
- }
-
+// Writes a memory buffer to 'target' Partition.
+static bool WriteBufferToPartition(const FileContents& file_contents, const Partition& partition) {
+ const unsigned char* data = file_contents.data.data();
+ size_t len = file_contents.data.size();
size_t start = 0;
bool success = false;
for (size_t attempt = 0; attempt < 2; ++attempt) {
- if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) {
- printf("failed seek on %s: %s\n", partition, strerror(errno));
- return -1;
- }
- while (start < len) {
- size_t to_write = len - start;
- if (to_write > 1 << 20) to_write = 1 << 20;
-
- ssize_t written = TEMP_FAILURE_RETRY(ota_write(fd, data + start, to_write));
- if (written == -1) {
- printf("failed write writing to %s: %s\n", partition, strerror(errno));
- return -1;
- }
- start += written;
- }
-
- if (ota_fsync(fd) != 0) {
- printf("failed to sync to %s: %s\n", partition, strerror(errno));
- return -1;
- }
- if (ota_close(fd) != 0) {
- printf("failed to close %s: %s\n", partition, strerror(errno));
- return -1;
- }
-
- fd.reset(ota_open(partition, O_RDONLY));
+ android::base::unique_fd fd(open(partition.name.c_str(), O_RDWR));
if (fd == -1) {
- printf("failed to reopen %s for verify: %s\n", partition, strerror(errno));
- return -1;
+ PLOG(ERROR) << "Failed to open \"" << partition << "\"";
+ return false;
+ }
+
+ if (TEMP_FAILURE_RETRY(lseek(fd, start, SEEK_SET)) == -1) {
+ PLOG(ERROR) << "Failed to seek to " << start << " on \"" << partition << "\"";
+ return false;
+ }
+
+ if (!android::base::WriteFully(fd, data + start, len - start)) {
+ PLOG(ERROR) << "Failed to write " << len - start << " bytes to \"" << partition << "\"";
+ return false;
+ }
+
+ if (fsync(fd) != 0) {
+ PLOG(ERROR) << "Failed to sync \"" << partition << "\"";
+ return false;
+ }
+ if (close(fd.release()) != 0) {
+ PLOG(ERROR) << "Failed to close \"" << partition << "\"";
+ return false;
+ }
+
+ fd.reset(open(partition.name.c_str(), O_RDONLY));
+ if (fd == -1) {
+ PLOG(ERROR) << "Failed to reopen \"" << partition << "\" for verification";
+ return false;
}
// Drop caches so our subsequent verification read won't just be reading the cache.
sync();
- unique_fd dc(ota_open("/proc/sys/vm/drop_caches", O_WRONLY));
- if (TEMP_FAILURE_RETRY(ota_write(dc, "3\n", 2)) == -1) {
- printf("write to /proc/sys/vm/drop_caches failed: %s\n", strerror(errno));
+ std::string drop_cache = "/proc/sys/vm/drop_caches";
+ if (!android::base::WriteStringToFile("3\n", drop_cache)) {
+ PLOG(ERROR) << "Failed to write to " << drop_cache;
} else {
- printf(" caches dropped\n");
+ LOG(INFO) << " caches dropped";
}
- ota_close(dc);
sleep(1);
// Verify.
if (TEMP_FAILURE_RETRY(lseek(fd, 0, SEEK_SET)) == -1) {
- printf("failed to seek back to beginning of %s: %s\n", partition, strerror(errno));
- return -1;
+ PLOG(ERROR) << "Failed to seek to 0 on " << partition;
+ return false;
}
unsigned char buffer[4096];
@@ -288,386 +194,262 @@
to_read = sizeof(buffer);
}
- size_t so_far = 0;
- while (so_far < to_read) {
- ssize_t read_count = TEMP_FAILURE_RETRY(ota_read(fd, buffer + so_far, to_read - so_far));
- if (read_count == -1) {
- printf("verify read error %s at %zu: %s\n", partition, p, strerror(errno));
- return -1;
- } else if (read_count == 0) {
- printf("verify read reached unexpected EOF, %s at %zu\n", partition, p);
- return -1;
- }
- if (static_cast<size_t>(read_count) < to_read) {
- printf("short verify read %s at %zu: %zd %zu\n", partition, p, read_count, to_read);
- }
- so_far += read_count;
+ if (!android::base::ReadFully(fd, buffer, to_read)) {
+ PLOG(ERROR) << "Failed to verify-read " << partition << " at " << p;
+ return false;
}
if (memcmp(buffer, data + p, to_read) != 0) {
- printf("verification failed starting at %zu\n", p);
+ LOG(ERROR) << "Verification failed starting at " << p;
start = p;
break;
}
}
if (start == len) {
- printf("verification read succeeded (attempt %zu)\n", attempt + 1);
+ LOG(INFO) << "Verification read succeeded (attempt " << attempt + 1 << ")";
success = true;
break;
}
- if (ota_close(fd) != 0) {
- printf("failed to close %s: %s\n", partition, strerror(errno));
- return -1;
- }
-
- fd.reset(ota_open(partition, O_RDWR));
- if (fd == -1) {
- printf("failed to reopen %s for retry write && verify: %s\n", partition, strerror(errno));
- return -1;
+ if (close(fd.release()) != 0) {
+ PLOG(ERROR) << "Failed to close " << partition;
+ return false;
}
}
if (!success) {
- printf("failed to verify after all attempts\n");
- return -1;
+ LOG(ERROR) << "Failed to verify after all attempts";
+ return false;
}
- if (ota_close(fd) == -1) {
- printf("error closing %s: %s\n", partition, strerror(errno));
- return -1;
- }
sync();
+ return true;
+}
+
+int ParseSha1(const std::string& str, uint8_t* digest) {
+ const char* ps = str.c_str();
+ uint8_t* pd = digest;
+ for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) {
+ int digit;
+ if (*ps >= '0' && *ps <= '9') {
+ digit = *ps - '0';
+ } else if (*ps >= 'a' && *ps <= 'f') {
+ digit = *ps - 'a' + 10;
+ } else if (*ps >= 'A' && *ps <= 'F') {
+ digit = *ps - 'A' + 10;
+ } else {
+ return -1;
+ }
+ if (i % 2 == 0) {
+ *pd = digit << 4;
+ } else {
+ *pd |= digit;
+ ++pd;
+ }
+ }
+ if (*ps != '\0') return -1;
return 0;
}
-// Take a string 'str' of 40 hex digits and parse it into the 20
-// byte array 'digest'. 'str' may contain only the digest or be of
-// the form "<digest>:<anything>". Return 0 on success, -1 on any
-// error.
-int ParseSha1(const char* str, uint8_t* digest) {
- const char* ps = str;
- uint8_t* pd = digest;
- for (int i = 0; i < SHA_DIGEST_LENGTH * 2; ++i, ++ps) {
- int digit;
- if (*ps >= '0' && *ps <= '9') {
- digit = *ps - '0';
- } else if (*ps >= 'a' && *ps <= 'f') {
- digit = *ps - 'a' + 10;
- } else if (*ps >= 'A' && *ps <= 'F') {
- digit = *ps - 'A' + 10;
- } else {
- return -1;
- }
- if (i % 2 == 0) {
- *pd = digit << 4;
- } else {
- *pd |= digit;
- ++pd;
- }
- }
- if (*ps != '\0') return -1;
- return 0;
-}
-
-// Search an array of sha1 strings for one matching the given sha1.
-// Return the index of the match on success, or -1 if no match is
-// found.
-static int FindMatchingPatch(uint8_t* sha1, const std::vector<std::string>& patch_sha1_str) {
- for (size_t i = 0; i < patch_sha1_str.size(); ++i) {
- uint8_t patch_sha1[SHA_DIGEST_LENGTH];
- if (ParseSha1(patch_sha1_str[i].c_str(), patch_sha1) == 0 &&
- memcmp(patch_sha1, sha1, SHA_DIGEST_LENGTH) == 0) {
- return i;
- }
- }
- return -1;
-}
-
-// Returns 0 if the contents of the file (argv[2]) or the cached file
-// match any of the sha1's on the command line (argv[3:]). Returns
-// nonzero otherwise.
-int applypatch_check(const char* filename, const std::vector<std::string>& patch_sha1_str) {
- FileContents file;
-
- // It's okay to specify no sha1s; the check will pass if the
- // LoadFileContents is successful. (Useful for reading
- // partitions, where the filename encodes the sha1s; no need to
- // check them twice.)
- if (LoadFileContents(filename, &file) != 0 ||
- (!patch_sha1_str.empty() && FindMatchingPatch(file.sha1, patch_sha1_str) < 0)) {
- printf("file \"%s\" doesn't have any of expected sha1 sums; checking cache\n", filename);
-
- // If the source file is missing or corrupted, it might be because we were killed in the middle
- // of patching it. A copy of it should have been made in cache_temp_source. If that file
- // exists and matches the sha1 we're looking for, the check still passes.
- if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), &file) != 0) {
- printf("failed to load cache file\n");
- return 1;
- }
-
- if (FindMatchingPatch(file.sha1, patch_sha1_str) < 0) {
- printf("cache bits don't match any sha1 for \"%s\"\n", filename);
- return 1;
- }
- }
- return 0;
+bool PatchPartitionCheck(const Partition& target, const Partition& source) {
+ FileContents target_file;
+ FileContents source_file;
+ return (ReadPartitionToBuffer(target, &target_file, false) ||
+ ReadPartitionToBuffer(source, &source_file, true));
}
int ShowLicenses() {
- ShowBSDiffLicense();
- return 0;
-}
-
-static size_t FileSink(const unsigned char* data, size_t len, int fd) {
- size_t done = 0;
- while (done < len) {
- ssize_t wrote = TEMP_FAILURE_RETRY(ota_write(fd, data + done, len - done));
- if (wrote == -1) {
- printf("error writing %zd bytes: %s\n", (len - done), strerror(errno));
- return done;
- }
- done += wrote;
- }
- return done;
-}
-
-// Return the amount of free space (in bytes) on the filesystem
-// containing filename. filename must exist. Return -1 on error.
-size_t FreeSpaceForFile(const char* filename) {
- struct statfs sf;
- if (statfs(filename, &sf) != 0) {
- printf("failed to statfs %s: %s\n", filename, strerror(errno));
- return -1;
- }
- return sf.f_bsize * sf.f_bavail;
-}
-
-int CacheSizeCheck(size_t bytes) {
- if (MakeFreeSpaceOnCache(bytes) < 0) {
- printf("unable to make %zu bytes available on /cache\n", bytes);
- return 1;
- }
- return 0;
-}
-
-// This function applies binary patches to EMMC target files in a way that is safe (the original
-// file is not touched until we have the desired replacement for it) and idempotent (it's okay to
-// run this program multiple times).
-//
-// - If the SHA-1 hash of <target_filename> is <target_sha1_string>, does nothing and exits
-// successfully.
-//
-// - Otherwise, if the SHA-1 hash of <source_filename> is one of the entries in <patch_sha1_str>,
-// the corresponding patch from <patch_data> (which must be a VAL_BLOB) is applied to produce a
-// new file (the type of patch is automatically detected from the blob data). If that new file
-// has SHA-1 hash <target_sha1_str>, moves it to replace <target_filename>, and exits
-// successfully. Note that if <source_filename> and <target_filename> are not the same,
-// <source_filename> is NOT deleted on success. <target_filename> may be the string "-" to mean
-// "the same as <source_filename>".
-//
-// - Otherwise, or if any error is encountered, exits with non-zero status.
-//
-// <source_filename> must refer to an EMMC partition to read the source data. See the comments for
-// the LoadPartitionContents() function above for the format of such a filename. <target_size> has
-// become obsolete since we have dropped the support for patching non-EMMC targets (EMMC targets
-// have the size embedded in the filename).
-int applypatch(const char* source_filename, const char* target_filename,
- const char* target_sha1_str, size_t /* target_size */,
- const std::vector<std::string>& patch_sha1_str,
- const std::vector<std::unique_ptr<Value>>& patch_data, const Value* bonus_data) {
- printf("patch %s: ", source_filename);
-
- if (target_filename[0] == '-' && target_filename[1] == '\0') {
- target_filename = source_filename;
- }
-
- if (strncmp(target_filename, "EMMC:", 5) != 0) {
- printf("Supporting patching EMMC targets only.\n");
- return 1;
- }
-
- uint8_t target_sha1[SHA_DIGEST_LENGTH];
- if (ParseSha1(target_sha1_str, target_sha1) != 0) {
- printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
- return 1;
- }
-
- // We try to load the target file into the source_file object.
- FileContents source_file;
- if (LoadFileContents(target_filename, &source_file) == 0) {
- if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
- // The early-exit case: the patch was already applied, this file has the desired hash, nothing
- // for us to do.
- printf("already %s\n", short_sha1(target_sha1).c_str());
- return 0;
- }
- }
-
- if (source_file.data.empty() ||
- (target_filename != source_filename && strcmp(target_filename, source_filename) != 0)) {
- // Need to load the source file: either we failed to load the target file, or we did but it's
- // different from the expected.
- source_file.data.clear();
- LoadFileContents(source_filename, &source_file);
- }
-
- if (!source_file.data.empty()) {
- int to_use = FindMatchingPatch(source_file.sha1, patch_sha1_str);
- if (to_use != -1) {
- return GenerateTarget(source_file, patch_data[to_use], target_filename, target_sha1,
- bonus_data);
- }
- }
-
- printf("source file is bad; trying copy\n");
-
- FileContents copy_file;
- if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), ©_file) < 0) {
- printf("failed to read copy file\n");
- return 1;
- }
-
- int to_use = FindMatchingPatch(copy_file.sha1, patch_sha1_str);
- if (to_use == -1) {
- printf("copy file doesn't match source SHA-1s either\n");
- return 1;
- }
-
- return GenerateTarget(copy_file, patch_data[to_use], target_filename, target_sha1, bonus_data);
-}
-
-/*
- * This function flashes a given image to the target partition. It verifies
- * the target cheksum first, and will return if target has the desired hash.
- * It checks the checksum of the given source image before flashing, and
- * verifies the target partition afterwards. The function is idempotent.
- * Returns zero on success.
- */
-int applypatch_flash(const char* source_filename, const char* target_filename,
- const char* target_sha1_str, size_t target_size) {
- printf("flash %s: ", target_filename);
-
- uint8_t target_sha1[SHA_DIGEST_LENGTH];
- if (ParseSha1(target_sha1_str, target_sha1) != 0) {
- printf("failed to parse tgt-sha1 \"%s\"\n", target_sha1_str);
- return 1;
- }
-
- std::string target_str(target_filename);
- std::vector<std::string> pieces = android::base::Split(target_str, ":");
- if (pieces.size() != 2 || pieces[0] != "EMMC") {
- printf("invalid target name \"%s\"", target_filename);
- return 1;
- }
-
- // Load the target into the source_file object to see if already applied.
- pieces.push_back(std::to_string(target_size));
- pieces.push_back(target_sha1_str);
- std::string fullname = android::base::Join(pieces, ':');
- FileContents source_file;
- if (LoadPartitionContents(fullname, &source_file) == 0 &&
- memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) == 0) {
- // The early-exit case: the image was already applied, this partition
- // has the desired hash, nothing for us to do.
- printf("already %s\n", short_sha1(target_sha1).c_str());
- return 0;
- }
-
- if (LoadFileContents(source_filename, &source_file) == 0) {
- if (memcmp(source_file.sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
- // The source doesn't have desired checksum.
- printf("source \"%s\" doesn't have expected sha1 sum\n", source_filename);
- printf("expected: %s, found: %s\n", short_sha1(target_sha1).c_str(),
- short_sha1(source_file.sha1).c_str());
- return 1;
- }
- }
-
- if (WriteToPartition(source_file.data.data(), target_size, target_filename) != 0) {
- printf("write of copied data to %s failed\n", target_filename);
- return 1;
- }
+ ShowBSDiffLicense();
return 0;
}
-static int GenerateTarget(const FileContents& source_file, const std::unique_ptr<Value>& patch,
- const std::string& target_filename,
- const uint8_t target_sha1[SHA_DIGEST_LENGTH], const Value* bonus_data) {
- if (patch->type != VAL_BLOB) {
- printf("patch is not a blob\n");
- return 1;
+bool PatchPartition(const Partition& target, const Partition& source, const Value& patch,
+ const Value* bonus) {
+ LOG(INFO) << "Patching " << target.name;
+
+ // We try to load and check against the target hash first.
+ FileContents target_file;
+ if (ReadPartitionToBuffer(target, &target_file, false)) {
+ // The early-exit case: the patch was already applied, this file has the desired hash, nothing
+ // for us to do.
+ LOG(INFO) << " already " << target.hash.substr(0, 8);
+ return true;
}
- const char* header = &patch->data[0];
- size_t header_bytes_read = patch->data.size();
+ FileContents source_file;
+ if (ReadPartitionToBuffer(source, &source_file, true)) {
+ return GenerateTarget(target, source_file, patch, bonus);
+ }
+
+ LOG(ERROR) << "Failed to find any match";
+ return false;
+}
+
+bool FlashPartition(const Partition& partition, const std::string& source_filename) {
+ LOG(INFO) << "Flashing " << partition;
+
+ // We try to load and check against the target hash first.
+ FileContents target_file;
+ if (ReadPartitionToBuffer(partition, &target_file, false)) {
+ // The early-exit case: the patch was already applied, this file has the desired hash, nothing
+ // for us to do.
+ LOG(INFO) << " already " << partition.hash.substr(0, 8);
+ return true;
+ }
+
+ FileContents source_file;
+ if (!LoadFileContents(source_filename, &source_file)) {
+ LOG(ERROR) << "Failed to load source file";
+ return false;
+ }
+
+ uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+ if (ParseSha1(partition.hash, expected_sha1) != 0) {
+ LOG(ERROR) << "Failed to parse source hash \"" << partition.hash << "\"";
+ return false;
+ }
+
+ if (memcmp(source_file.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) {
+ // The source doesn't have desired checksum.
+ LOG(ERROR) << "source \"" << source_filename << "\" doesn't have expected SHA-1 sum";
+ LOG(ERROR) << "expected: " << partition.hash.substr(0, 8)
+ << ", found: " << short_sha1(source_file.sha1);
+ return false;
+ }
+ if (!WriteBufferToPartition(source_file, partition)) {
+ LOG(ERROR) << "Failed to write to " << partition;
+ return false;
+ }
+ return true;
+}
+
+static bool GenerateTarget(const Partition& target, const FileContents& source_file,
+ const Value& patch, const Value* bonus_data) {
+ uint8_t expected_sha1[SHA_DIGEST_LENGTH];
+ if (ParseSha1(target.hash, expected_sha1) != 0) {
+ LOG(ERROR) << "Failed to parse target hash \"" << target.hash << "\"";
+ return false;
+ }
+
+ if (patch.type != Value::Type::BLOB) {
+ LOG(ERROR) << "patch is not a blob";
+ return false;
+ }
+
+ const char* header = patch.data.data();
+ size_t header_bytes_read = patch.data.size();
bool use_bsdiff = false;
if (header_bytes_read >= 8 && memcmp(header, "BSDIFF40", 8) == 0) {
use_bsdiff = true;
} else if (header_bytes_read >= 8 && memcmp(header, "IMGDIFF2", 8) == 0) {
use_bsdiff = false;
} else {
- printf("Unknown patch file format\n");
- return 1;
+ LOG(ERROR) << "Unknown patch file format";
+ return false;
}
- CHECK(android::base::StartsWith(target_filename, "EMMC:"));
-
- // We still write the original source to cache, in case the partition write is interrupted.
- if (MakeFreeSpaceOnCache(source_file.data.size()) < 0) {
- printf("not enough free space on /cache\n");
- return 1;
+ // We write the original source to cache, in case the partition write is interrupted.
+ if (!CheckAndFreeSpaceOnCache(source_file.data.size())) {
+ LOG(ERROR) << "Not enough free space on /cache";
+ return false;
}
- if (SaveFileContents(CacheLocation::location().cache_temp_source().c_str(), &source_file) < 0) {
- printf("failed to back up source file\n");
- return 1;
+ if (!SaveFileContents(Paths::Get().cache_temp_source(), &source_file)) {
+ LOG(ERROR) << "Failed to back up source file";
+ return false;
}
// We store the decoded output in memory.
- std::string memory_sink_str; // Don't need to reserve space.
- SinkFn sink = [&memory_sink_str](const unsigned char* data, size_t len) {
- memory_sink_str.append(reinterpret_cast<const char*>(data), len);
+ FileContents patched;
+ SHA_CTX ctx;
+ SHA1_Init(&ctx);
+ SinkFn sink = [&patched, &ctx](const unsigned char* data, size_t len) {
+ SHA1_Update(&ctx, data, len);
+ patched.data.insert(patched.data.end(), data, data + len);
return len;
};
- SHA_CTX ctx;
- SHA1_Init(&ctx);
-
int result;
if (use_bsdiff) {
- result =
- ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), *patch, 0, sink, &ctx);
+ result = ApplyBSDiffPatch(source_file.data.data(), source_file.data.size(), patch, 0, sink);
} else {
- result = ApplyImagePatch(source_file.data.data(), source_file.data.size(), *patch, sink, &ctx,
- bonus_data);
+ result =
+ ApplyImagePatch(source_file.data.data(), source_file.data.size(), patch, sink, bonus_data);
}
if (result != 0) {
- printf("applying patch failed\n");
- return 1;
+ LOG(ERROR) << "Failed to apply the patch: " << result;
+ return false;
}
- uint8_t current_target_sha1[SHA_DIGEST_LENGTH];
- SHA1_Final(current_target_sha1, &ctx);
- if (memcmp(current_target_sha1, target_sha1, SHA_DIGEST_LENGTH) != 0) {
- printf("patch did not produce expected sha1\n");
- return 1;
- } else {
- printf("now %s\n", short_sha1(target_sha1).c_str());
+ SHA1_Final(patched.sha1, &ctx);
+ if (memcmp(patched.sha1, expected_sha1, SHA_DIGEST_LENGTH) != 0) {
+ LOG(ERROR) << "Patching did not produce the expected SHA-1 of " << short_sha1(expected_sha1);
+
+ LOG(ERROR) << "target size " << patched.data.size() << " SHA-1 " << short_sha1(patched.sha1);
+ LOG(ERROR) << "source size " << source_file.data.size() << " SHA-1 "
+ << short_sha1(source_file.sha1);
+
+ uint8_t patch_digest[SHA_DIGEST_LENGTH];
+ SHA1(reinterpret_cast<const uint8_t*>(patch.data.data()), patch.data.size(), patch_digest);
+ LOG(ERROR) << "patch size " << patch.data.size() << " SHA-1 " << short_sha1(patch_digest);
+
+ if (bonus_data != nullptr) {
+ uint8_t bonus_digest[SHA_DIGEST_LENGTH];
+ SHA1(reinterpret_cast<const uint8_t*>(bonus_data->data.data()), bonus_data->data.size(),
+ bonus_digest);
+ LOG(ERROR) << "bonus size " << bonus_data->data.size() << " SHA-1 "
+ << short_sha1(bonus_digest);
+ }
+
+ return false;
}
+ LOG(INFO) << " now " << short_sha1(expected_sha1);
+
// Write back the temp file to the partition.
- if (WriteToPartition(reinterpret_cast<const unsigned char*>(memory_sink_str.c_str()),
- memory_sink_str.size(), target_filename) != 0) {
- printf("write of patched data to %s failed\n", target_filename.c_str());
- return 1;
+ if (!WriteBufferToPartition(patched, target)) {
+ LOG(ERROR) << "Failed to write patched data to " << target.name;
+ return false;
}
// Delete the backup copy of the source.
- unlink(CacheLocation::location().cache_temp_source().c_str());
+ unlink(Paths::Get().cache_temp_source().c_str());
// Success!
- return 0;
+ return true;
+}
+
+bool CheckPartition(const Partition& partition) {
+ FileContents target_file;
+ return ReadPartitionToBuffer(partition, &target_file, false);
+}
+
+Partition Partition::Parse(const std::string& input_str, std::string* err) {
+ std::vector<std::string> pieces = android::base::Split(input_str, ":");
+ if (pieces.size() != 4 || pieces[0] != "EMMC") {
+ *err = "Invalid number of tokens or non-eMMC target";
+ return {};
+ }
+
+ size_t size;
+ if (!android::base::ParseUint(pieces[2], &size) || size == 0) {
+ *err = "Failed to parse \"" + pieces[2] + "\" as byte count";
+ return {};
+ }
+
+ return Partition(pieces[1], size, pieces[3]);
+}
+
+std::string Partition::ToString() const {
+ if (*this) {
+ return "EMMC:"s + name + ":" + std::to_string(size) + ":" + hash;
+ }
+ return "<invalid-partition>";
+}
+
+std::ostream& operator<<(std::ostream& os, const Partition& partition) {
+ os << partition.ToString();
+ return os;
}
diff --git a/applypatch/applypatch_main.cpp b/applypatch/applypatch_main.cpp
index 197077c..92d2b3f 100644
--- a/applypatch/applypatch_main.cpp
+++ b/applypatch/applypatch_main.cpp
@@ -16,13 +16,10 @@
#include "applypatch_modes.h"
-// This program (applypatch) applies binary patches to files in a way that
-// is safe (the original file is not touched until we have the desired
-// replacement for it) and idempotent (it's okay to run this program
-// multiple times).
-//
-// See the comments to applypatch_modes() function.
+#include <android-base/logging.h>
+// See the comments for applypatch() function.
int main(int argc, char** argv) {
- return applypatch_modes(argc, const_cast<const char**>(argv));
+ android::base::InitLogging(argv);
+ return applypatch_modes(argc, argv);
}
diff --git a/applypatch/applypatch_modes.cpp b/applypatch/applypatch_modes.cpp
index aa32d57..b466598 100644
--- a/applypatch/applypatch_modes.cpp
+++ b/applypatch/applypatch_modes.cpp
@@ -16,6 +16,7 @@
#include "applypatch_modes.h"
+#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@@ -25,6 +26,8 @@
#include <string>
#include <vector>
+#include <android-base/file.h>
+#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/strings.h>
#include <openssl/sha.h>
@@ -32,157 +35,153 @@
#include "applypatch/applypatch.h"
#include "edify/expr.h"
-static int CheckMode(int argc, const char** argv) {
- if (argc < 3) {
+static int CheckMode(const std::string& target_emmc) {
+ std::string err;
+ auto target = Partition::Parse(target_emmc, &err);
+ if (!target) {
+ LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+ return 2;
+ }
+ return CheckPartition(target) ? 0 : 1;
+}
+
+static int FlashMode(const std::string& target_emmc, const std::string& source_file) {
+ std::string err;
+ auto target = Partition::Parse(target_emmc, &err);
+ if (!target) {
+ LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+ return 2;
+ }
+ return FlashPartition(target, source_file) ? 0 : 1;
+}
+
+static int PatchMode(const std::string& target_emmc, const std::string& source_emmc,
+ const std::string& patch_file, const std::string& bonus_file) {
+ std::string err;
+ auto target = Partition::Parse(target_emmc, &err);
+ if (!target) {
+ LOG(ERROR) << "Failed to parse target \"" << target_emmc << "\": " << err;
+ return 2;
+ }
+
+ auto source = Partition::Parse(source_emmc, &err);
+ if (!source) {
+ LOG(ERROR) << "Failed to parse source \"" << source_emmc << "\": " << err;
+ return 2;
+ }
+
+ std::string patch_contents;
+ if (!android::base::ReadFileToString(patch_file, &patch_contents)) {
+ PLOG(ERROR) << "Failed to read patch file \"" << patch_file << "\"";
+ return 1;
+ }
+
+ Value patch(Value::Type::BLOB, std::move(patch_contents));
+ std::unique_ptr<Value> bonus;
+ if (!bonus_file.empty()) {
+ std::string bonus_contents;
+ if (!android::base::ReadFileToString(bonus_file, &bonus_contents)) {
+ PLOG(ERROR) << "Failed to read bonus file \"" << bonus_file << "\"";
+ return 1;
+ }
+ bonus = std::make_unique<Value>(Value::Type::BLOB, std::move(bonus_contents));
+ }
+
+ return PatchPartition(target, source, patch, bonus.get()) ? 0 : 1;
+}
+
+static void Usage() {
+ printf(
+ "Usage: \n"
+ "check mode\n"
+ " applypatch --check EMMC:<target-file>:<target-size>:<target-sha1>\n\n"
+ "flash mode\n"
+ " applypatch --flash <source-file>\n"
+ " --target EMMC:<target-file>:<target-size>:<target-sha1>\n\n"
+ "patch mode\n"
+ " applypatch [--bonus <bonus-file>]\n"
+ " --patch <patch-file>\n"
+ " --target EMMC:<target-file>:<target-size>:<target-sha1>\n"
+ " --source EMMC:<source-file>:<source-size>:<source-sha1>\n\n"
+ "show license\n"
+ " applypatch --license\n"
+ "\n\n");
+}
+
+int applypatch_modes(int argc, char* argv[]) {
+ static constexpr struct option OPTIONS[]{
+ // clang-format off
+ { "bonus", required_argument, nullptr, 0 },
+ { "check", required_argument, nullptr, 0 },
+ { "flash", required_argument, nullptr, 0 },
+ { "license", no_argument, nullptr, 0 },
+ { "patch", required_argument, nullptr, 0 },
+ { "source", required_argument, nullptr, 0 },
+ { "target", required_argument, nullptr, 0 },
+ { nullptr, 0, nullptr, 0 },
+ // clang-format on
+ };
+
+ std::string check_target;
+ std::string source;
+ std::string target;
+ std::string patch;
+ std::string bonus;
+
+ bool check_mode = false;
+ bool flash_mode = false;
+ bool patch_mode = false;
+
+ optind = 1;
+
+ int arg;
+ int option_index;
+ while ((arg = getopt_long(argc, argv, "", OPTIONS, &option_index)) != -1) {
+ switch (arg) {
+ case 0: {
+ std::string option = OPTIONS[option_index].name;
+ if (option == "bonus") {
+ bonus = optarg;
+ } else if (option == "check") {
+ check_target = optarg;
+ check_mode = true;
+ } else if (option == "flash") {
+ source = optarg;
+ flash_mode = true;
+ } else if (option == "license") {
+ return ShowLicenses();
+ } else if (option == "patch") {
+ patch = optarg;
+ patch_mode = true;
+ } else if (option == "source") {
+ source = optarg;
+ } else if (option == "target") {
+ target = optarg;
+ }
+ break;
+ }
+ case '?':
+ default:
+ LOG(ERROR) << "Invalid argument";
+ Usage();
return 2;
}
- std::vector<std::string> sha1;
- for (int i = 3; i < argc; i++) {
- sha1.push_back(argv[i]);
+ }
+
+ if (check_mode) {
+ return CheckMode(check_target);
+ }
+ if (flash_mode) {
+ if (!bonus.empty()) {
+ LOG(ERROR) << "bonus file not supported in flash mode";
+ return 1;
}
+ return FlashMode(target, source);
+ }
+ if (patch_mode) {
+ return PatchMode(target, source, patch, bonus);
+ }
- return applypatch_check(argv[2], sha1);
-}
-
-// Parse arguments (which should be of the form "<sha1>:<filename>" into the
-// new parallel arrays *sha1s and *files. Returns true on success.
-static bool ParsePatchArgs(int argc, const char** argv, std::vector<std::string>* sha1s,
- std::vector<FileContents>* files) {
- if (sha1s == nullptr) {
- return false;
- }
- for (int i = 0; i < argc; ++i) {
- std::vector<std::string> pieces = android::base::Split(argv[i], ":");
- if (pieces.size() != 2) {
- printf("failed to parse patch argument \"%s\"\n", argv[i]);
- return false;
- }
-
- uint8_t digest[SHA_DIGEST_LENGTH];
- if (ParseSha1(pieces[0].c_str(), digest) != 0) {
- printf("failed to parse sha1 \"%s\"\n", argv[i]);
- return false;
- }
-
- sha1s->push_back(pieces[0]);
- FileContents fc;
- if (LoadFileContents(pieces[1].c_str(), &fc) != 0) {
- return false;
- }
- files->push_back(std::move(fc));
- }
- return true;
-}
-
-static int FlashMode(const char* src_filename, const char* tgt_filename,
- const char* tgt_sha1, size_t tgt_size) {
- return applypatch_flash(src_filename, tgt_filename, tgt_sha1, tgt_size);
-}
-
-static int PatchMode(int argc, const char** argv) {
- FileContents bonusFc;
- Value bonus(VAL_INVALID, "");
-
- if (argc >= 3 && strcmp(argv[1], "-b") == 0) {
- if (LoadFileContents(argv[2], &bonusFc) != 0) {
- printf("failed to load bonus file %s\n", argv[2]);
- return 1;
- }
- bonus.type = VAL_BLOB;
- bonus.data = std::string(bonusFc.data.cbegin(), bonusFc.data.cend());
- argc -= 2;
- argv += 2;
- }
-
- if (argc < 4) {
- return 2;
- }
-
- size_t target_size;
- if (!android::base::ParseUint(argv[4], &target_size) || target_size == 0) {
- printf("can't parse \"%s\" as byte count\n\n", argv[4]);
- return 1;
- }
-
- // If no <src-sha1>:<patch> is provided, it is in flash mode.
- if (argc == 5) {
- if (bonus.type != VAL_INVALID) {
- printf("bonus file not supported in flash mode\n");
- return 1;
- }
- return FlashMode(argv[1], argv[2], argv[3], target_size);
- }
-
- std::vector<std::string> sha1s;
- std::vector<FileContents> files;
- if (!ParsePatchArgs(argc-5, argv+5, &sha1s, &files)) {
- printf("failed to parse patch args\n");
- return 1;
- }
-
- std::vector<std::unique_ptr<Value>> patches;
- for (size_t i = 0; i < files.size(); ++i) {
- patches.push_back(std::make_unique<Value>(
- VAL_BLOB, std::string(files[i].data.cbegin(), files[i].data.cend())));
- }
- return applypatch(argv[1], argv[2], argv[3], target_size, sha1s, patches, &bonus);
-}
-
-// This program (applypatch) applies binary patches to files in a way that
-// is safe (the original file is not touched until we have the desired
-// replacement for it) and idempotent (it's okay to run this program
-// multiple times).
-//
-// - if the sha1 hash of <tgt-file> is <tgt-sha1>, does nothing and exits
-// successfully.
-//
-// - otherwise, if no <src-sha1>:<patch> is provided, flashes <tgt-file> with
-// <src-file>. <tgt-file> must be a partition name, while <src-file> must
-// be a regular image file. <src-file> will not be deleted on success.
-//
-// - otherwise, if the sha1 hash of <src-file> is <src-sha1>, applies the
-// bsdiff <patch> to <src-file> to produce a new file (the type of patch
-// is automatically detected from the file header). If that new
-// file has sha1 hash <tgt-sha1>, moves it to replace <tgt-file>, and
-// exits successfully. Note that if <src-file> and <tgt-file> are
-// not the same, <src-file> is NOT deleted on success. <tgt-file>
-// may be the string "-" to mean "the same as src-file".
-//
-// - otherwise, or if any error is encountered, exits with non-zero
-// status.
-//
-// <src-file> (or <file> in check mode) may refer to an EMMC partition
-// to read the source data. See the comments for the
-// LoadPartitionContents() function for the format of such a filename.
-
-int applypatch_modes(int argc, const char** argv) {
- if (argc < 2) {
- usage:
- printf(
- "usage: %s [-b <bonus-file>] <src-file> <tgt-file> <tgt-sha1> <tgt-size> "
- "[<src-sha1>:<patch> ...]\n"
- " or %s -c <file> [<sha1> ...]\n"
- " or %s -l\n"
- "\n"
- "Filenames may be of the form\n"
- " EMMC:<partition>:<len_1>:<sha1_1>:<len_2>:<sha1_2>:...\n"
- "to specify reading from or writing to an EMMC partition.\n\n",
- argv[0], argv[0], argv[0]);
- return 2;
- }
-
- int result;
-
- if (strncmp(argv[1], "-l", 3) == 0) {
- result = ShowLicenses();
- } else if (strncmp(argv[1], "-c", 3) == 0) {
- result = CheckMode(argc, argv);
- } else {
- result = PatchMode(argc, argv);
- }
-
- if (result == 2) {
- goto usage;
- }
- return result;
+ Usage();
+ return 2;
}
diff --git a/applypatch/applypatch_modes.h b/applypatch/applypatch_modes.h
index 3d9d08d..aa60a43 100644
--- a/applypatch/applypatch_modes.h
+++ b/applypatch/applypatch_modes.h
@@ -17,6 +17,6 @@
#ifndef _APPLYPATCH_MODES_H
#define _APPLYPATCH_MODES_H
-int applypatch_modes(int argc, const char** argv);
+int applypatch_modes(int argc, char* argv[]);
#endif // _APPLYPATCH_MODES_H
diff --git a/applypatch/bspatch.cpp b/applypatch/bspatch.cpp
index 912dbbd..ba33c3a 100644
--- a/applypatch/bspatch.cpp
+++ b/applypatch/bspatch.cpp
@@ -66,18 +66,12 @@
}
int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch,
- size_t patch_offset, SinkFn sink, SHA_CTX* ctx) {
- auto sha_sink = [&sink, &ctx](const uint8_t* data, size_t len) {
- len = sink(data, len);
- if (ctx) SHA1_Update(ctx, data, len);
- return len;
- };
-
+ size_t patch_offset, SinkFn sink) {
CHECK_LE(patch_offset, patch.data.size());
int result = bsdiff::bspatch(old_data, old_size,
reinterpret_cast<const uint8_t*>(&patch.data[patch_offset]),
- patch.data.size() - patch_offset, sha_sink);
+ patch.data.size() - patch_offset, sink);
if (result != 0) {
LOG(ERROR) << "bspatch failed, result: " << result;
// print SHA1 of the patch in the case of a data error.
diff --git a/applypatch/freecache.cpp b/applypatch/freecache.cpp
index ea364d8..e487865 100644
--- a/applypatch/freecache.cpp
+++ b/applypatch/freecache.cpp
@@ -14,31 +14,35 @@
* limitations under the License.
*/
+#include <dirent.h>
#include <errno.h>
-#include <libgen.h>
+#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/statfs.h>
#include <unistd.h>
-#include <dirent.h>
-#include <ctype.h>
+#include <algorithm>
+#include <limits>
#include <memory>
#include <set>
#include <string>
+#include <android-base/file.h>
+#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
#include "applypatch/applypatch.h"
-#include "otautil/cache_location.h"
+#include "otautil/paths.h"
-static int EliminateOpenFiles(std::set<std::string>* files) {
+static int EliminateOpenFiles(const std::string& dirname, std::set<std::string>* files) {
std::unique_ptr<DIR, decltype(&closedir)> d(opendir("/proc"), closedir);
if (!d) {
- printf("error opening /proc: %s\n", strerror(errno));
+ PLOG(ERROR) << "Failed to open /proc";
return -1;
}
struct dirent* de;
@@ -52,7 +56,7 @@
struct dirent* fdde;
std::unique_ptr<DIR, decltype(&closedir)> fdd(opendir(path.c_str()), closedir);
if (!fdd) {
- printf("error opening %s: %s\n", path.c_str(), strerror(errno));
+ PLOG(ERROR) << "Failed to open " << path;
continue;
}
while ((fdde = readdir(fdd.get())) != 0) {
@@ -62,9 +66,9 @@
int count = readlink(fd_path.c_str(), link, sizeof(link)-1);
if (count >= 0) {
link[count] = '\0';
- if (strncmp(link, "/cache/", 7) == 0) {
+ if (android::base::StartsWith(link, dirname)) {
if (files->erase(link) > 0) {
- printf("%s is open by %s\n", link, de->d_name);
+ LOG(INFO) << link << " is open by " << de->d_name;
}
}
}
@@ -73,77 +77,163 @@
return 0;
}
-static std::set<std::string> FindExpendableFiles() {
- std::set<std::string> files;
- // We're allowed to delete unopened regular files in any of these
- // directories.
- const char* dirs[2] = {"/cache", "/cache/recovery/otatest"};
+static std::vector<std::string> FindExpendableFiles(
+ const std::string& dirname, const std::function<bool(const std::string&)>& name_filter) {
+ std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir);
+ if (!d) {
+ PLOG(ERROR) << "Failed to open " << dirname;
+ return {};
+ }
- for (size_t i = 0; i < sizeof(dirs)/sizeof(dirs[0]); ++i) {
- std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirs[i]), closedir);
- if (!d) {
- printf("error opening %s: %s\n", dirs[i], strerror(errno));
+ // Look for regular files in the directory (not in any subdirectories).
+ std::set<std::string> files;
+ struct dirent* de;
+ while ((de = readdir(d.get())) != 0) {
+ std::string path = dirname + "/" + de->d_name;
+
+ // We can't delete cache_temp_source; if it's there we might have restarted during
+ // installation and could be depending on it to be there.
+ if (path == Paths::Get().cache_temp_source()) {
continue;
}
- // Look for regular files in the directory (not in any subdirectories).
- struct dirent* de;
- while ((de = readdir(d.get())) != 0) {
- std::string path = std::string(dirs[i]) + "/" + de->d_name;
+ // Do not delete the file if it doesn't have the expected format.
+ if (name_filter != nullptr && !name_filter(de->d_name)) {
+ continue;
+ }
- // We can't delete cache_temp_source; if it's there we might have restarted during
- // installation and could be depending on it to be there.
- if (path == CacheLocation::location().cache_temp_source()) {
- continue;
- }
-
- struct stat st;
- if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
- files.insert(path);
- }
+ struct stat st;
+ if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+ files.insert(path);
}
}
- printf("%zu regular files in deletable directories\n", files.size());
- if (EliminateOpenFiles(&files) < 0) {
- return std::set<std::string>();
+ LOG(INFO) << files.size() << " regular files in deletable directory";
+ if (EliminateOpenFiles(dirname, &files) < 0) {
+ return {};
}
- return files;
+
+ return std::vector<std::string>(files.begin(), files.end());
}
-int MakeFreeSpaceOnCache(size_t bytes_needed) {
-#ifndef __ANDROID__
- // TODO (xunchang) implement a heuristic cache size check during host simulation.
- printf("Skip making (%zu) bytes free space on cache; program is running on host\n", bytes_needed);
- return 0;
-#endif
-
- size_t free_now = FreeSpaceForFile("/cache");
- printf("%zu bytes free on /cache (%zu needed)\n", free_now, bytes_needed);
-
- if (free_now >= bytes_needed) {
+// Parses the index of given log file, e.g. 3 for last_log.3; returns max number if the log name
+// doesn't have the expected format so that we'll delete these ones first.
+static unsigned int GetLogIndex(const std::string& log_name) {
+ if (log_name == "last_log" || log_name == "last_kmsg") {
return 0;
}
- std::set<std::string> files = FindExpendableFiles();
- if (files.empty()) {
- // nothing we can delete to free up space!
- printf("no files can be deleted to free space on /cache\n");
+
+ unsigned int index;
+ if (sscanf(log_name.c_str(), "last_log.%u", &index) == 1 ||
+ sscanf(log_name.c_str(), "last_kmsg.%u", &index) == 1) {
+ return index;
+ }
+
+ return std::numeric_limits<unsigned int>::max();
+}
+
+// Returns the amount of free space (in bytes) on the filesystem containing filename, or -1 on
+// error.
+static int64_t FreeSpaceForFile(const std::string& filename) {
+ struct statfs sf;
+ if (statfs(filename.c_str(), &sf) == -1) {
+ PLOG(ERROR) << "Failed to statfs " << filename;
return -1;
}
- // We could try to be smarter about which files to delete: the
- // biggest ones? the smallest ones that will free up enough space?
- // the oldest? the newest?
- //
- // Instead, we'll be dumb.
+ 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) {
+ LOG(ERROR) << "Invalid block size or overflow (sf.f_bsize " << sf.f_bsize << ", sf.f_bavail "
+ << sf.f_bavail << ")";
+ return -1;
+ }
+ return free_space;
+}
- for (const auto& file : files) {
- unlink(file.c_str());
- free_now = FreeSpaceForFile("/cache");
- printf("deleted %s; now %zu bytes free\n", file.c_str(), free_now);
- if (free_now < bytes_needed) {
- break;
+bool CheckAndFreeSpaceOnCache(size_t bytes) {
+#ifndef __ANDROID__
+ // TODO(xunchang): Implement a heuristic cache size check during host simulation.
+ LOG(WARNING) << "Skipped making (" << bytes
+ << ") bytes free space on /cache; program is running on host";
+ return true;
+#endif
+
+ std::vector<std::string> dirs{ "/cache", Paths::Get().cache_log_directory() };
+ for (const auto& dirname : dirs) {
+ if (RemoveFilesInDirectory(bytes, dirname, FreeSpaceForFile)) {
+ return true;
}
}
- return (free_now >= bytes_needed) ? 0 : -1;
+
+ return false;
+}
+
+bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname,
+ const std::function<int64_t(const std::string&)>& space_checker) {
+ struct stat st;
+ if (stat(dirname.c_str(), &st) == -1) {
+ PLOG(ERROR) << "Failed to stat " << dirname;
+ return false;
+ }
+ if (!S_ISDIR(st.st_mode)) {
+ LOG(ERROR) << dirname << " is not a directory";
+ return false;
+ }
+
+ int64_t free_now = space_checker(dirname);
+ if (free_now == -1) {
+ LOG(ERROR) << "Failed to check free space for " << dirname;
+ return false;
+ }
+ LOG(INFO) << free_now << " bytes free on " << dirname << " (" << bytes_needed << " needed)";
+
+ if (free_now >= bytes_needed) {
+ return true;
+ }
+
+ std::vector<std::string> files;
+ if (dirname == Paths::Get().cache_log_directory()) {
+ // Deletes the log files only.
+ auto log_filter = [](const std::string& file_name) {
+ return android::base::StartsWith(file_name, "last_log") ||
+ android::base::StartsWith(file_name, "last_kmsg");
+ };
+
+ files = FindExpendableFiles(dirname, log_filter);
+
+ // Older logs will come to the top of the queue.
+ auto comparator = [](const std::string& name1, const std::string& name2) -> bool {
+ unsigned int index1 = GetLogIndex(android::base::Basename(name1));
+ unsigned int index2 = GetLogIndex(android::base::Basename(name2));
+ if (index1 == index2) {
+ return name1 < name2;
+ }
+
+ return index1 > index2;
+ };
+
+ std::sort(files.begin(), files.end(), comparator);
+ } else {
+ // We're allowed to delete unopened regular files in the directory.
+ files = FindExpendableFiles(dirname, nullptr);
+ }
+
+ for (const auto& file : files) {
+ if (unlink(file.c_str()) == -1) {
+ PLOG(ERROR) << "Failed to delete " << file;
+ continue;
+ }
+
+ free_now = space_checker(dirname);
+ if (free_now == -1) {
+ LOG(ERROR) << "Failed to check free space for " << dirname;
+ return false;
+ }
+ LOG(INFO) << "Deleted " << file << "; now " << free_now << " bytes free";
+ if (free_now >= bytes_needed) {
+ return true;
+ }
+ }
+
+ return false;
}
diff --git a/applypatch/imgdiff.cpp b/applypatch/imgdiff.cpp
index 674cc2b..415d95f 100644
--- a/applypatch/imgdiff.cpp
+++ b/applypatch/imgdiff.cpp
@@ -462,12 +462,12 @@
target_len_(tgt.GetRawDataLength()),
target_uncompressed_len_(tgt.DataLengthForPatch()),
target_compress_level_(tgt.GetCompressLevel()),
- data_(tgt.DataForPatch(), tgt.DataForPatch() + tgt.DataLengthForPatch()) {}
+ data_(tgt.GetRawData(), tgt.GetRawData() + tgt.GetRawDataLength()) {}
// Return true if raw data is smaller than the patch size.
bool PatchChunk::RawDataIsSmaller(const ImageChunk& tgt, size_t patch_size) {
size_t target_len = tgt.GetRawDataLength();
- return (tgt.GetType() == CHUNK_NORMAL && (target_len <= 160 || target_len < patch_size));
+ return target_len < patch_size || (tgt.GetType() == CHUNK_NORMAL && target_len <= 160);
}
void PatchChunk::UpdateSourceOffset(const SortedRangeSet& src_range) {
diff --git a/applypatch/imgpatch.cpp b/applypatch/imgpatch.cpp
index 3682d61..e6be39a 100644
--- a/applypatch/imgpatch.cpp
+++ b/applypatch/imgpatch.cpp
@@ -38,6 +38,7 @@
#include <zlib.h>
#include "edify/expr.h"
+#include "otautil/print_sha1.h"
static inline int64_t Read8(const void *address) {
return android::base::get_unaligned<int64_t>(address);
@@ -51,8 +52,9 @@
// patched data and stream the deflated data to output.
static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_len,
const Value& patch, size_t patch_offset,
- const char* deflate_header, SinkFn sink, SHA_CTX* ctx) {
+ const char* deflate_header, SinkFn sink) {
size_t expected_target_length = static_cast<size_t>(Read8(deflate_header + 32));
+ CHECK_GT(expected_target_length, 0);
int level = Read4(deflate_header + 40);
int method = Read4(deflate_header + 44);
int window_bits = Read4(deflate_header + 48);
@@ -77,7 +79,7 @@
size_t total_written = 0;
static constexpr size_t buffer_size = 32768;
auto compression_sink = [&strm, &actual_target_length, &expected_target_length, &total_written,
- &ret, &ctx, &sink](const uint8_t* data, size_t len) -> size_t {
+ &ret, &sink](const uint8_t* data, size_t len) -> size_t {
// The input patch length for an update never exceeds INT_MAX.
strm.avail_in = len;
strm.next_in = data;
@@ -102,15 +104,13 @@
LOG(ERROR) << "Failed to write " << have << " compressed bytes to output.";
return 0;
}
- if (ctx) SHA1_Update(ctx, buffer.data(), have);
} while ((strm.avail_in != 0 || strm.avail_out == 0) && ret != Z_STREAM_END);
actual_target_length += len;
return len;
};
- int bspatch_result =
- ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink, nullptr);
+ int bspatch_result = ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink);
deflateEnd(&strm);
if (bspatch_result != 0) {
@@ -127,19 +127,20 @@
<< actual_target_length;
return false;
}
- LOG(DEBUG) << "bspatch writes " << total_written << " bytes in total to streaming output.";
+ LOG(DEBUG) << "bspatch wrote " << total_written << " bytes in total to streaming output.";
return true;
}
int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const unsigned char* patch_data,
size_t patch_size, SinkFn sink) {
- Value patch(VAL_BLOB, std::string(reinterpret_cast<const char*>(patch_data), patch_size));
- return ApplyImagePatch(old_data, old_size, patch, sink, nullptr, nullptr);
+ Value patch(Value::Type::BLOB,
+ std::string(reinterpret_cast<const char*>(patch_data), patch_size));
+ return ApplyImagePatch(old_data, old_size, patch, sink, nullptr);
}
int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink,
- SHA_CTX* ctx, const Value* bonus_data) {
+ const Value* bonus_data) {
if (patch.data.size() < 12) {
printf("patch too short to contain header\n");
return -1;
@@ -180,10 +181,12 @@
printf("source data too short\n");
return -1;
}
- if (ApplyBSDiffPatch(old_data + src_start, src_len, patch, patch_offset, sink, ctx) != 0) {
+ if (ApplyBSDiffPatch(old_data + src_start, src_len, patch, patch_offset, sink) != 0) {
printf("Failed to apply bsdiff patch.\n");
return -1;
}
+
+ LOG(DEBUG) << "Processed chunk type normal";
} else if (type == CHUNK_RAW) {
const char* raw_header = patch_header + pos;
pos += 4;
@@ -198,14 +201,13 @@
printf("failed to read chunk %d raw data\n", i);
return -1;
}
- if (ctx) {
- SHA1_Update(ctx, patch_header + pos, data_len);
- }
if (sink(reinterpret_cast<const unsigned char*>(patch_header + pos), data_len) != data_len) {
printf("failed to write chunk %d raw data\n", i);
return -1;
}
pos += data_len;
+
+ LOG(DEBUG) << "Processed chunk type raw";
} else if (type == CHUNK_DEFLATE) {
// deflate chunks have an additional 60 bytes in their chunk header.
const char* deflate_header = patch_header + pos;
@@ -228,11 +230,10 @@
// Decompress the source data; the chunk header tells us exactly
// how big we expect it to be when decompressed.
- // Note: expanded_len will include the bonus data size if
- // the patch was constructed with bonus data. The
- // deflation will come up 'bonus_size' bytes short; these
- // must be appended from the bonus_data value.
- size_t bonus_size = (i == 1 && bonus_data != NULL) ? bonus_data->data.size() : 0;
+ // Note: expanded_len will include the bonus data size if the patch was constructed with
+ // bonus data. The deflation will come up 'bonus_size' bytes short; these must be appended
+ // from the bonus_data value.
+ size_t bonus_size = (i == 1 && bonus_data != nullptr) ? bonus_data->data.size() : 0;
std::vector<unsigned char> expanded_source(expanded_len);
@@ -270,17 +271,18 @@
inflateEnd(&strm);
if (bonus_size) {
- memcpy(expanded_source.data() + (expanded_len - bonus_size), &bonus_data->data[0],
+ memcpy(expanded_source.data() + (expanded_len - bonus_size), bonus_data->data.data(),
bonus_size);
}
}
if (!ApplyBSDiffPatchAndStreamOutput(expanded_source.data(), expanded_len, patch,
- patch_offset, deflate_header, sink, ctx)) {
+ patch_offset, deflate_header, sink)) {
LOG(ERROR) << "Fail to apply streaming bspatch.";
return -1;
}
+ LOG(DEBUG) << "Processed chunk type deflate";
} else {
printf("patch chunk %d is unknown type %d\n", i, type);
return -1;
diff --git a/applypatch/include/applypatch/applypatch.h b/applypatch/include/applypatch/applypatch.h
index 912ead1..6fc6f0f 100644
--- a/applypatch/include/applypatch/applypatch.h
+++ b/applypatch/include/applypatch/applypatch.h
@@ -21,6 +21,7 @@
#include <functional>
#include <memory>
+#include <ostream>
#include <string>
#include <vector>
@@ -39,45 +40,91 @@
// applypatch.cpp
int ShowLicenses();
-size_t FreeSpaceForFile(const char* filename);
-int CacheSizeCheck(size_t bytes);
-int ParseSha1(const char* str, uint8_t* digest);
-int applypatch(const char* source_filename,
- const char* target_filename,
- const char* target_sha1_str,
- size_t target_size,
- const std::vector<std::string>& patch_sha1_str,
- const std::vector<std::unique_ptr<Value>>& patch_data,
- const Value* bonus_data);
-int applypatch_check(const char* filename,
- const std::vector<std::string>& patch_sha1_str);
-int applypatch_flash(const char* source_filename, const char* target_filename,
- const char* target_sha1_str, size_t target_size);
+// Parses a given string of 40 hex digits into 20-byte array 'digest'. 'str' may contain only the
+// digest or be of the form "<digest>:<anything>". Returns 0 on success, or -1 on any error.
+int ParseSha1(const std::string& str, uint8_t* digest);
-int LoadFileContents(const char* filename, FileContents* file);
-int SaveFileContents(const char* filename, const FileContents* file);
+struct Partition {
+ Partition() = default;
+
+ Partition(const std::string& name, size_t size, const std::string& hash)
+ : name(name), size(size), hash(hash) {}
+
+ // Parses and returns the given string into a Partition object. The input string is of the form
+ // "EMMC:<device>:<size>:<hash>". Returns the parsed Partition, or an empty object on error.
+ static Partition Parse(const std::string& partition, std::string* err);
+
+ std::string ToString() const;
+
+ // Returns whether the current Partition object is valid.
+ explicit operator bool() const {
+ return !name.empty();
+ }
+
+ std::string name;
+ size_t size;
+ std::string hash;
+};
+
+std::ostream& operator<<(std::ostream& os, const Partition& partition);
+
+// Applies the given 'patch' to the 'source' Partition, verifies then writes the patching result to
+// the 'target' Partition. While patching, it will backup the data on the source partition to
+// /cache, so that the patching could be resumed on interruption even if both of the source and
+// target partitions refer to the same device. The function is idempotent if called multiple times.
+// An optional arg 'bonus' can be provided, if the patch was generated with a bonus output.
+// Returns the patching result.
+bool PatchPartition(const Partition& target, const Partition& source, const Value& patch,
+ const Value* bonus);
+
+// Returns whether the contents of the eMMC target or the cached file match the embedded hash.
+// It will look for the backup on /cache if the given partition doesn't match the checksum.
+bool PatchPartitionCheck(const Partition& target, const Partition& source);
+
+// Checks whether the contents of the given partition has the desired hash. It will NOT look for
+// the backup on /cache if the given partition doesn't have the expected checksum.
+bool CheckPartition(const Partition& target);
+
+// Flashes a given image in 'source_filename' to the eMMC target partition. It verifies the target
+// checksum first, and will return if target already has the desired hash. Otherwise it checks the
+// checksum of the given source image, flashes, and verifies the target partition afterwards. The
+// function is idempotent. Returns the flashing result.
+bool FlashPartition(const Partition& target, const std::string& source_filename);
+
+// Reads a file into memory; stores the file contents and associated metadata in *file.
+bool LoadFileContents(const std::string& filename, FileContents* file);
+
+// Saves the given FileContents object to the given filename.
+bool SaveFileContents(const std::string& filename, const FileContents* file);
// bspatch.cpp
void ShowBSDiffLicense();
// Applies the bsdiff-patch given in 'patch' (from offset 'patch_offset' to the end) to the source
-// data given by (old_data, old_size). Writes the patched output through the given 'sink', and
-// updates the SHA-1 context with the output data. Returns 0 on success.
+// data given by (old_data, old_size). Writes the patched output through the given 'sink'. Returns
+// 0 on success.
int ApplyBSDiffPatch(const unsigned char* old_data, size_t old_size, const Value& patch,
- size_t patch_offset, SinkFn sink, SHA_CTX* ctx);
+ size_t patch_offset, SinkFn sink);
// imgpatch.cpp
// Applies the imgdiff-patch given in 'patch' to the source data given by (old_data, old_size), with
-// the optional bonus data. Writes the patched output through the given 'sink', and updates the
-// SHA-1 context with the output data. Returns 0 on success.
+// the optional bonus data. Writes the patched output through the given 'sink'. Returns 0 on
+// success.
int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value& patch, SinkFn sink,
- SHA_CTX* ctx, const Value* bonus_data);
+ const Value* bonus_data);
// freecache.cpp
-int MakeFreeSpaceOnCache(size_t bytes_needed);
+// Checks whether /cache partition has at least 'bytes'-byte free space. Returns true immediately
+// if so. Otherwise, it will try to free some space by removing older logs, checks again and
+// returns the checking result.
+bool CheckAndFreeSpaceOnCache(size_t bytes);
+// Removes the files in |dirname| until we have at least |bytes_needed| bytes of free space on the
+// partition. |space_checker| should return the size of the free space, or -1 on error.
+bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname,
+ const std::function<int64_t(const std::string&)>& space_checker);
#endif
diff --git a/applypatch/include/applypatch/imgdiff_image.h b/applypatch/include/applypatch/imgdiff_image.h
index 0848072..6716051 100644
--- a/applypatch/include/applypatch/imgdiff_image.h
+++ b/applypatch/include/applypatch/imgdiff_image.h
@@ -44,6 +44,8 @@
int GetType() const {
return type_;
}
+
+ const uint8_t* GetRawData() const;
size_t GetRawDataLength() const {
return raw_data_len_;
}
@@ -99,7 +101,6 @@
bsdiff::SuffixArrayIndexInterface** bsdiff_cache);
private:
- const uint8_t* GetRawData() const;
bool TryReconstruction(int level);
int type_; // CHUNK_NORMAL, CHUNK_DEFLATE, CHUNK_RAW
diff --git a/boot_control/Android.bp b/boot_control/Android.bp
new file mode 100644
index 0000000..7720ead
--- /dev/null
+++ b/boot_control/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+cc_library_shared {
+ name: "bootctrl.default",
+ recovery_available: true,
+ relative_install_path: "hw",
+
+ srcs: ["boot_control.cpp"],
+
+ cflags: [
+ "-D_FILE_OFFSET_BITS=64",
+ "-Werror",
+ "-Wall",
+ "-Wextra",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libbootloader_message",
+ "libfs_mgr",
+ "liblog",
+ ],
+}
diff --git a/boot_control/Android.mk b/boot_control/Android.mk
deleted file mode 100644
index 9814d71..0000000
--- a/boot_control/Android.mk
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-# Copyright (C) 2017 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(my-dir)
-
-include $(CLEAR_VARS)
-LOCAL_MODULE := bootctrl.bcb
-LOCAL_MODULE_RELATIVE_PATH := hw
-LOCAL_SRC_FILES := boot_control.cpp
-LOCAL_CFLAGS := \
- -D_FILE_OFFSET_BITS=64 \
- -Werror \
- -Wall \
- -Wextra
-LOCAL_SHARED_LIBRARIES := liblog
-LOCAL_STATIC_LIBRARIES := libbootloader_message libfs_mgr libbase
-LOCAL_POST_INSTALL_CMD := \
- $(hide) mkdir -p $(TARGET_OUT_SHARED_LIBRARIES)/hw && \
- ln -sf bootctrl.bcb.so $(TARGET_OUT_SHARED_LIBRARIES)/hw/bootctrl.default.so
-include $(BUILD_SHARED_LIBRARY)
diff --git a/bootloader_message/Android.bp b/bootloader_message/Android.bp
index c81c67b..5cd2132 100644
--- a/bootloader_message/Android.bp
+++ b/bootloader_message/Android.bp
@@ -14,14 +14,15 @@
// limitations under the License.
//
-cc_library_static {
+cc_library {
name: "libbootloader_message",
+ recovery_available: true,
srcs: ["bootloader_message.cpp"],
cflags: [
"-Wall",
"-Werror",
],
- static_libs: [
+ shared_libs: [
"libbase",
"libfs_mgr",
],
diff --git a/common.h b/common.h
index 8b336f8..38bdd52 100644
--- a/common.h
+++ b/common.h
@@ -17,8 +17,8 @@
#ifndef RECOVERY_COMMON_H
#define RECOVERY_COMMON_H
-#include <stdio.h>
#include <stdarg.h>
+#include <stdio.h>
#include <string>
@@ -27,9 +27,12 @@
static constexpr int kRecoveryApiVersion = 3;
class RecoveryUI;
+struct selabel_handle;
+extern struct selabel_handle* sehandle;
extern RecoveryUI* ui;
extern bool modified_flash;
+extern bool has_cache;
// The current stage, e.g. "1/2".
extern std::string stage;
@@ -37,13 +40,9 @@
// The reason argument provided in "--reason=".
extern const char* reason;
-// fopen a file, mounting volumes and making parent dirs as necessary.
-FILE* fopen_path(const char *path, const char *mode);
-
-void ui_print(const char* format, ...);
+void ui_print(const char* format, ...) __printflike(1, 2);
bool is_ro_debuggable();
-bool reboot(const std::string& command);
-
+bool SetUsbConfig(const std::string& state);
#endif // RECOVERY_COMMON_H
diff --git a/device.cpp b/device.cpp
index f881daf..eec1932 100644
--- a/device.cpp
+++ b/device.cpp
@@ -16,50 +16,58 @@
#include "device.h"
-static const char* MENU_ITEMS[] = {
- "Reboot system now",
- "Reboot to bootloader",
- "Apply update from ADB",
- "Apply update from SD card",
- "Wipe data/factory reset",
-#ifndef AB_OTA_UPDATER
- "Wipe cache partition",
-#endif // !AB_OTA_UPDATER
- "Mount /system",
- "View recovery logs",
- "Run graphics test",
- "Run locale test",
- "Power off",
- nullptr,
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <android-base/logging.h>
+
+#include "ui.h"
+
+static std::vector<std::pair<std::string, Device::BuiltinAction>> g_menu_actions{
+ { "Reboot system now", Device::REBOOT },
+ { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
+ { "Enter fastboot", Device::ENTER_FASTBOOT },
+ { "Apply update from ADB", Device::APPLY_ADB_SIDELOAD },
+ { "Apply update from SD card", Device::APPLY_SDCARD },
+ { "Wipe data/factory reset", Device::WIPE_DATA },
+ { "Wipe cache partition", Device::WIPE_CACHE },
+ { "Mount /system", Device::MOUNT_SYSTEM },
+ { "View recovery logs", Device::VIEW_RECOVERY_LOGS },
+ { "Run graphics test", Device::RUN_GRAPHICS_TEST },
+ { "Run locale test", Device::RUN_LOCALE_TEST },
+ { "Power off", Device::SHUTDOWN },
};
-static const Device::BuiltinAction MENU_ACTIONS[] = {
- Device::REBOOT,
- Device::REBOOT_BOOTLOADER,
- Device::APPLY_ADB_SIDELOAD,
- Device::APPLY_SDCARD,
- Device::WIPE_DATA,
-#ifndef AB_OTA_UPDATER
- Device::WIPE_CACHE,
-#endif // !AB_OTA_UPDATER
- Device::MOUNT_SYSTEM,
- Device::VIEW_RECOVERY_LOGS,
- Device::RUN_GRAPHICS_TEST,
- Device::RUN_LOCALE_TEST,
- Device::SHUTDOWN,
-};
+static std::vector<std::string> g_menu_items;
-static_assert(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]) ==
- sizeof(MENU_ACTIONS) / sizeof(MENU_ACTIONS[0]) + 1,
- "MENU_ITEMS and MENU_ACTIONS should have the same length, "
- "except for the extra NULL entry in MENU_ITEMS.");
-
-const char* const* Device::GetMenuItems() {
- return MENU_ITEMS;
+static void PopulateMenuItems() {
+ g_menu_items.clear();
+ std::transform(g_menu_actions.cbegin(), g_menu_actions.cend(), std::back_inserter(g_menu_items),
+ [](const auto& entry) { return entry.first; });
}
-Device::BuiltinAction Device::InvokeMenuItem(int menu_position) {
- return menu_position < 0 ? NO_ACTION : MENU_ACTIONS[menu_position];
+Device::Device(RecoveryUI* ui) : ui_(ui) {
+ PopulateMenuItems();
+}
+
+void Device::RemoveMenuItemForAction(Device::BuiltinAction action) {
+ g_menu_actions.erase(
+ std::remove_if(g_menu_actions.begin(), g_menu_actions.end(),
+ [action](const auto& entry) { return entry.second == action; }));
+ CHECK(!g_menu_actions.empty());
+
+ // Re-populate the menu items.
+ PopulateMenuItems();
+}
+
+const std::vector<std::string>& Device::GetMenuItems() {
+ return g_menu_items;
+}
+
+Device::BuiltinAction Device::InvokeMenuItem(size_t menu_position) {
+ return g_menu_actions[menu_position].second;
}
int Device::HandleMenuKey(int key, bool visible) {
diff --git a/device.h b/device.h
index 74745b3..6a8daf8 100644
--- a/device.h
+++ b/device.h
@@ -17,41 +17,21 @@
#ifndef _RECOVERY_DEVICE_H
#define _RECOVERY_DEVICE_H
-#include "ui.h"
+#include <stddef.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+// Forward declaration to avoid including "ui.h".
+class RecoveryUI;
class Device {
public:
- explicit Device(RecoveryUI* ui) : ui_(ui) {}
- virtual ~Device() {}
-
- // Called to obtain the UI object that should be used to display the recovery user interface for
- // this device. You should not have called Init() on the UI object already, the caller will do
- // that after this method returns.
- virtual RecoveryUI* GetUI() {
- return ui_;
- }
-
- // Called when recovery starts up (after the UI has been obtained and initialized and after the
- // arguments have been parsed, but before anything else).
- virtual void StartRecovery() {};
-
- // Called from the main thread when recovery is at the main menu and waiting for input, and a key
- // is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
- // recovery will be at the main menu with it invisible after an unsuccessful operation [ie OTA
- // package failure], or if recovery is started with no command.)
- //
- // 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
- // object you returned from GetUI if you want to find out if other keys are held down.)
- //
- // 'visible' is true if the menu is visible.
- //
- // Returns one of the defined constants below in order to:
- //
- // - move the menu highlight (kHighlight{Up,Down})
- // - invoke the highlighted item (kInvokeItem)
- // - do nothing (kNoAction)
- // - invoke a specific action (a menu position: any non-negative number)
- virtual int HandleMenuKey(int key, bool visible);
+ static constexpr const int kNoAction = -1;
+ static constexpr const int kHighlightUp = -2;
+ static constexpr const int kHighlightDown = -3;
+ static constexpr const int kInvokeItem = -4;
enum BuiltinAction {
NO_ACTION = 0,
@@ -67,24 +47,61 @@
MOUNT_SYSTEM = 10,
RUN_GRAPHICS_TEST = 11,
RUN_LOCALE_TEST = 12,
+ KEY_INTERRUPTED = 13,
+ ENTER_FASTBOOT = 14,
+ ENTER_RECOVERY = 15,
};
- // Return the list of menu items (an array of strings, NULL-terminated). The menu_position passed
- // to InvokeMenuItem will correspond to the indexes into this array.
- virtual const char* const* GetMenuItems();
+ explicit Device(RecoveryUI* ui);
+ virtual ~Device() {}
- // Perform a recovery action selected from the menu. 'menu_position' will be the item number of
- // the selected menu item, or a non-negative number returned from HandleMenuKey(). The menu will
- // be hidden when this is called; implementations can call ui_print() to print information to the
+ // Returns a raw pointer to the RecoveryUI object.
+ virtual RecoveryUI* GetUI() {
+ return ui_.get();
+ }
+
+ // Resets the UI object to the given UI. Used to override the default UI in case initialization
+ // failed, or we want a different UI for some reason. The device object will take the ownership.
+ virtual void ResetUI(RecoveryUI* ui) {
+ ui_.reset(ui);
+ }
+
+ // Called when recovery starts up (after the UI has been obtained and initialized and after the
+ // arguments have been parsed, but before anything else).
+ virtual void StartRecovery() {};
+
+ // Called from the main thread when recovery is at the main menu and waiting for input, and a key
+ // is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
+ // recovery will be at the main menu with it invisible after an unsuccessful operation, such as
+ // failed to install an OTA package, or if recovery is started with no command.)
+ //
+ // 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
+ // object you returned from GetUI() if you want to find out if other keys are held down.)
+ //
+ // 'visible' is true if the menu is visible.
+ //
+ // Returns one of the defined constants below in order to:
+ // - move the menu highlight (kHighlight{Up,Down}: negative value)
+ // - invoke the highlighted item (kInvokeItem: negative value)
+ // - do nothing (kNoAction: negative value)
+ // - invoke a specific action (a menu position: non-negative value)
+ virtual int HandleMenuKey(int key, bool visible);
+
+ // Returns the list of menu items (a vector of strings). The menu_position passed to
+ // InvokeMenuItem() will correspond to the indexes into this array.
+ virtual const std::vector<std::string>& GetMenuItems();
+
+ // Performs a recovery action selected from the menu. 'menu_position' will be the index of the
+ // selected menu item, or a non-negative value returned from HandleMenuKey(). The menu will be
+ // hidden when this is called; implementations can call ui_print() to print information to the
// screen. If the menu position is one of the builtin actions, you can just return the
// corresponding enum value. If it is an action specific to your device, you actually perform it
// here and return NO_ACTION.
- virtual BuiltinAction InvokeMenuItem(int menu_position);
+ virtual BuiltinAction InvokeMenuItem(size_t menu_position);
- static const int kNoAction = -1;
- static const int kHighlightUp = -2;
- static const int kHighlightDown = -3;
- static const int kInvokeItem = -4;
+ // Removes the menu item for the given action. This allows tailoring the menu based on the
+ // runtime info, such as the availability of /cache or /sdcard.
+ virtual void RemoveMenuItemForAction(Device::BuiltinAction action);
// Called before and after we do a wipe data/factory reset operation, either via a reboot from the
// main system with the --wipe_data flag, or when the user boots into recovery image manually and
@@ -100,11 +117,16 @@
}
private:
- RecoveryUI* ui_;
+ // The RecoveryUI object that should be used to display the user interface for this device.
+ std::unique_ptr<RecoveryUI> ui_;
};
+// Disable name mangling, as this function will be loaded via dlsym(3).
+extern "C" {
+
// The device-specific library must define this function (or the default one will be used, if there
// is no device-specific library). It returns the Device object that recovery should use.
Device* make_device();
+}
#endif // _DEVICE_H
diff --git a/edify/expr.cpp b/edify/expr.cpp
index 6823b73..c090eb2 100644
--- a/edify/expr.cpp
+++ b/edify/expr.cpp
@@ -51,9 +51,9 @@
if (!v) {
return false;
}
- if (v->type != VAL_STRING) {
- ErrorAbort(state, kArgsParsingFailure, "expecting string, got value type %d", v->type);
- return false;
+ if (v->type != Value::Type::STRING) {
+ ErrorAbort(state, kArgsParsingFailure, "expecting string, got value type %d", v->type);
+ return false;
}
*result = v->data;
@@ -68,7 +68,7 @@
if (str == nullptr) {
return nullptr;
}
- return new Value(VAL_STRING, str);
+ return new Value(Value::Type::STRING, str);
}
Value* StringValue(const std::string& str) {
diff --git a/edify/include/edify/expr.h b/edify/include/edify/expr.h
index 770d1cf..5cbd5e1 100644
--- a/edify/include/edify/expr.h
+++ b/edify/include/edify/expr.h
@@ -53,19 +53,16 @@
bool is_retry = false;
};
-enum ValueType {
- VAL_INVALID = -1,
- VAL_STRING = 1,
- VAL_BLOB = 2,
-};
-
struct Value {
- ValueType type;
- std::string data;
+ enum class Type {
+ STRING = 1,
+ BLOB = 2,
+ };
- Value(ValueType type, const std::string& str) :
- type(type),
- data(str) {}
+ Value(Type type, const std::string& str) : type(type), data(str) {}
+
+ Type type;
+ std::string data;
};
struct Expr;
@@ -156,6 +153,6 @@
Value* StringValue(const std::string& str);
-int parse_string(const char* str, std::unique_ptr<Expr>* root, int* error_count);
+int ParseString(const std::string& str, std::unique_ptr<Expr>* root, int* error_count);
#endif // _EXPRESSION_H
diff --git a/edify/parser.yy b/edify/parser.yy
index bd2e010..3a63c37 100644
--- a/edify/parser.yy
+++ b/edify/parser.yy
@@ -138,7 +138,7 @@
++*error_count;
}
-int parse_string(const char* str, std::unique_ptr<Expr>* root, int* error_count) {
- yy_switch_to_buffer(yy_scan_string(str));
- return yyparse(root, error_count);
+int ParseString(const std::string& str, std::unique_ptr<Expr>* root, int* error_count) {
+ yy_switch_to_buffer(yy_scan_string(str.c_str()));
+ return yyparse(root, error_count);
}
diff --git a/etc/init.rc b/etc/init.rc
index 0fc6c4c..fa8fe26 100644
--- a/etc/init.rc
+++ b/etc/init.rc
@@ -6,11 +6,17 @@
start ueventd
+ setprop sys.usb.configfs 0
+
on init
export ANDROID_ROOT /system
export ANDROID_DATA /data
export EXTERNAL_STORAGE /sdcard
+ symlink /proc/self/fd/0 /dev/stdin
+ symlink /proc/self/fd/1 /dev/stdout
+ symlink /proc/self/fd/2 /dev/stderr
+
symlink /system/bin /bin
symlink /system/etc /etc
@@ -22,6 +28,7 @@
mkdir /data
mkdir /cache
mkdir /sideload
+ mkdir /mnt/system
mount tmpfs tmpfs /tmp
chown root shell /tmp
@@ -30,20 +37,6 @@
write /proc/sys/kernel/panic_on_oops 1
write /proc/sys/vm/max_map_count 1000000
-on fs
- write /sys/class/android_usb/android0/f_ffs/aliases adb
- mkdir /dev/usb-ffs 0770 shell shell
- mkdir /dev/usb-ffs/adb 0770 shell shell
- mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
-
- write /sys/class/android_usb/android0/enable 0
- write /sys/class/android_usb/android0/idVendor 18D1
- write /sys/class/android_usb/android0/idProduct D001
- write /sys/class/android_usb/android0/functions adb
- write /sys/class/android_usb/android0/iManufacturer ${ro.product.manufacturer}
- write /sys/class/android_usb/android0/iProduct ${ro.product.model}
- write /sys/class/android_usb/android0/iSerial ${ro.serialno}
-
on boot
ifup lo
hostname localhost
@@ -76,7 +69,7 @@
trigger early-boot
trigger boot
-service ueventd /sbin/ueventd
+service ueventd /system/bin/ueventd
critical
seclabel u:r:ueventd:s0
@@ -84,21 +77,111 @@
critical
seclabel u:r:charger:s0
-service recovery /sbin/recovery
+service recovery /system/bin/recovery
+ socket recovery stream 422 system system
seclabel u:r:recovery:s0
-service adbd /sbin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery
+service adbd /system/bin/adbd --root_seclabel=u:r:su:s0 --device_banner=recovery
disabled
socket adbd stream 660 system system
seclabel u:r:adbd:s0
-# Always start adbd on userdebug and eng builds
-on property:ro.debuggable=1
- write /sys/class/android_usb/android0/enable 1
- start adbd
+service fastbootd /system/bin/fastbootd
+ disabled
+ group system
+ seclabel u:r:fastbootd:s0
# Restart adbd so it can run as root
on property:service.adb.root=1
- write /sys/class/android_usb/android0/enable 0
restart adbd
+
+# Always start adbd on userdebug and eng builds
+on fs && property:ro.debuggable=1
+ setprop sys.usb.config adb
+
+on fs && property:sys.usb.configfs=1
+ mount configfs none /config
+ mkdir /config/usb_gadget/g1 0770 shell shell
+ write /config/usb_gadget/g1/idVendor 0x18D1
+ mkdir /config/usb_gadget/g1/strings/0x409 0770
+ write /config/usb_gadget/g1/strings/0x409/serialnumber ${ro.serialno}
+ write /config/usb_gadget/g1/strings/0x409/manufacturer ${ro.product.manufacturer}
+ write /config/usb_gadget/g1/strings/0x409/product ${ro.product.model}
+ mkdir /config/usb_gadget/g1/functions/ffs.adb
+ mkdir /config/usb_gadget/g1/functions/ffs.fastboot
+ mkdir /config/usb_gadget/g1/configs/b.1 0777 shell shell
+ mkdir /config/usb_gadget/g1/configs/b.1/strings/0x409 0770 shell shell
+
+on fs && property:sys.usb.configfs=0
+ write /sys/class/android_usb/android0/f_ffs/aliases adb,fastboot
+ write /sys/class/android_usb/android0/idVendor 18D1
+ write /sys/class/android_usb/android0/iManufacturer ${ro.product.manufacturer}
+ write /sys/class/android_usb/android0/iProduct ${ro.product.model}
+ write /sys/class/android_usb/android0/iSerial ${ro.serialno}
+
+on fs
+ mkdir /dev/usb-ffs 0775 shell shell
+ mkdir /dev/usb-ffs/adb 0770 shell shell
+ mount functionfs adb /dev/usb-ffs/adb uid=2000,gid=2000
+ mkdir /dev/usb-ffs/fastboot 0770 system system
+ mount functionfs fastboot /dev/usb-ffs/fastboot rmode=0770,fmode=0660,uid=1000,gid=1000
+
+on property:sys.usb.config=adb
+ start adbd
+
+on property:sys.usb.config=fastboot
+ start fastbootd
+
+on property:sys.usb.config=none && property:sys.usb.configfs=0
+ stop adbd
+ stop fastboot
+ write /sys/class/android_usb/android0/enable 0
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=adb && property:sys.usb.configfs=0
+ write /sys/class/android_usb/android0/idProduct D001
+ write /sys/class/android_usb/android0/functions adb
write /sys/class/android_usb/android0/enable 1
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=sideload && property:sys.usb.configfs=0
+ write /sys/class/android_usb/android0/idProduct D001
+ write /sys/class/android_usb/android0/functions adb
+ write /sys/class/android_usb/android0/enable 1
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=fastboot && property:sys.usb.configfs=0
+ write /sys/class/android_usb/android0/idProduct 4EE0
+ write /sys/class/android_usb/android0/functions fastboot
+ write /sys/class/android_usb/android0/enable 1
+ setprop sys.usb.state ${sys.usb.config}
+
+# Configfs triggers
+on property:sys.usb.config=none && property:sys.usb.configfs=1
+ write /config/usb_gadget/g1/UDC "none"
+ stop adbd
+ stop fastbootd
+ setprop sys.usb.ffs.ready 0
+ rm /config/usb_gadget/g1/configs/b.1/f1
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=sideload && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+ write /config/usb_gadget/g1/idProduct 0xD001
+ write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "adb"
+ symlink /config/usb_gadget/g1/functions/ffs.adb /config/usb_gadget/g1/configs/b.1/f1
+ write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=adb && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+ write /config/usb_gadget/g1/idProduct 0xD001
+ write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "adb"
+ symlink /config/usb_gadget/g1/functions/ffs.adb /config/usb_gadget/g1/configs/b.1/f1
+ write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+ setprop sys.usb.state ${sys.usb.config}
+
+on property:sys.usb.config=fastboot && property:sys.usb.ffs.ready=1 && property:sys.usb.configfs=1
+ write /config/usb_gadget/g1/idProduct 0x4EE0
+ write /config/usb_gadget/g1/configs/b.1/strings/0x409/configuration "fastboot"
+ symlink /config/usb_gadget/g1/functions/ffs.fastboot /config/usb_gadget/g1/configs/b.1/f1
+ write /config/usb_gadget/g1/UDC ${sys.usb.controller}
+ setprop sys.usb.state ${sys.usb.config}
diff --git a/fastboot/fastboot.cpp b/fastboot/fastboot.cpp
new file mode 100644
index 0000000..8458c99
--- /dev/null
+++ b/fastboot/fastboot.cpp
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "fastboot.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <algorithm>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <bootloader_message/bootloader_message.h>
+
+#include "device.h"
+#include "ui.h"
+
+static const std::vector<std::pair<std::string, Device::BuiltinAction>> kFastbootMenuActions{
+ { "Reboot system now", Device::REBOOT },
+ { "Enter recovery", Device::ENTER_RECOVERY },
+ { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
+ { "Power off", Device::SHUTDOWN },
+};
+
+Device::BuiltinAction StartFastboot(Device* device, const std::vector<std::string>& /* args */) {
+ RecoveryUI* ui = device->GetUI();
+
+ std::vector<std::string> title_lines = { "Android Fastboot" };
+ title_lines.push_back("Product name - " + android::base::GetProperty("ro.product.device", ""));
+ title_lines.push_back("Bootloader version - " + android::base::GetProperty("ro.bootloader", ""));
+ title_lines.push_back("Baseband version - " +
+ android::base::GetProperty("ro.build.expect.baseband", ""));
+ title_lines.push_back("Serial number - " + android::base::GetProperty("ro.serialno", ""));
+ title_lines.push_back(std::string("Secure boot - ") +
+ ((android::base::GetProperty("ro.secure", "") == "1") ? "yes" : "no"));
+ title_lines.push_back("HW version - " + android::base::GetProperty("ro.revision", ""));
+
+ ui->ResetKeyInterruptStatus();
+ ui->SetTitle(title_lines);
+ ui->ShowText(true);
+
+ // Reset to normal system boot so recovery won't cycle indefinitely.
+ // TODO(b/112277594) Clear only if 'recovery' field of BCB is empty. If not,
+ // set the 'command' field of BCB to 'boot-recovery' so the next boot is into recovery
+ // to finish any interrupted tasks.
+ std::string err;
+ if (!clear_bootloader_message(&err)) {
+ LOG(ERROR) << "Failed to clear BCB message: " << err;
+ }
+
+ std::vector<std::string> fastboot_menu_items;
+ std::transform(kFastbootMenuActions.cbegin(), kFastbootMenuActions.cend(),
+ std::back_inserter(fastboot_menu_items),
+ [](const auto& entry) { return entry.first; });
+
+ auto chosen_item = ui->ShowMenu(
+ {}, fastboot_menu_items, 0, false,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+ return Device::KEY_INTERRUPTED;
+ }
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT)) {
+ return Device::BuiltinAction::NO_ACTION;
+ }
+ return kFastbootMenuActions[chosen_item].second;
+}
diff --git a/mounts.h b/fastboot/fastboot.h
similarity index 66%
copy from mounts.h
copy to fastboot/fastboot.h
index 0de1ebd..53a2adc 100644
--- a/mounts.h
+++ b/fastboot/fastboot.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,11 @@
* limitations under the License.
*/
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
-struct MountedVolume;
+#include <string>
+#include <vector>
-bool scan_mounted_volumes();
+#include "device.h"
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+Device::BuiltinAction StartFastboot(Device* device, const std::vector<std::string>& args);
diff --git a/fsck_unshare_blocks.cpp b/fsck_unshare_blocks.cpp
new file mode 100644
index 0000000..2e6b5b8
--- /dev/null
+++ b/fsck_unshare_blocks.cpp
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "fsck_unshare_blocks.h"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <spawn.h>
+#include <string.h>
+#include <sys/mount.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/unique_fd.h>
+#include <fstab/fstab.h>
+
+#include "roots.h"
+
+static constexpr const char* SYSTEM_E2FSCK_BIN = "/system/bin/e2fsck_static";
+static constexpr const char* TMP_E2FSCK_BIN = "/tmp/e2fsck.bin";
+
+static bool copy_file(const char* source, const char* dest) {
+ android::base::unique_fd source_fd(open(source, O_RDONLY));
+ if (source_fd < 0) {
+ PLOG(ERROR) << "open %s failed" << source;
+ return false;
+ }
+
+ android::base::unique_fd dest_fd(open(dest, O_CREAT | O_WRONLY, S_IRWXU));
+ if (dest_fd < 0) {
+ PLOG(ERROR) << "open %s failed" << dest;
+ return false;
+ }
+
+ for (;;) {
+ char buf[4096];
+ ssize_t rv = read(source_fd, buf, sizeof(buf));
+ if (rv < 0) {
+ PLOG(ERROR) << "read failed";
+ return false;
+ }
+ if (rv == 0) {
+ break;
+ }
+ if (write(dest_fd, buf, rv) != rv) {
+ PLOG(ERROR) << "write failed";
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool run_e2fsck(const std::string& partition) {
+ Volume* volume = volume_for_mount_point(partition);
+ if (!volume) {
+ LOG(INFO) << "No fstab entry for " << partition << ", skipping.";
+ return true;
+ }
+
+ LOG(INFO) << "Running e2fsck on device " << volume->blk_device;
+
+ std::vector<std::string> args = { TMP_E2FSCK_BIN, "-p", "-E", "unshare_blocks",
+ volume->blk_device };
+ std::vector<char*> argv(args.size());
+ std::transform(args.cbegin(), args.cend(), argv.begin(),
+ [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+ argv.push_back(nullptr);
+
+ pid_t child;
+ char* env[] = { nullptr };
+ if (posix_spawn(&child, argv[0], nullptr, nullptr, argv.data(), env)) {
+ PLOG(ERROR) << "posix_spawn failed";
+ return false;
+ }
+
+ int status = 0;
+ int ret = TEMP_FAILURE_RETRY(waitpid(child, &status, 0));
+ if (ret < 0) {
+ PLOG(ERROR) << "waitpid failed";
+ return false;
+ }
+ if (!WIFEXITED(status)) {
+ LOG(ERROR) << "e2fsck exited abnormally: " << status;
+ return false;
+ }
+ int return_code = WEXITSTATUS(status);
+ if (return_code >= 8) {
+ LOG(ERROR) << "e2fsck could not unshare blocks: " << return_code;
+ return false;
+ }
+
+ LOG(INFO) << "Successfully unshared blocks on " << partition;
+ return true;
+}
+
+bool do_fsck_unshare_blocks() {
+ // List of partitions we will try to e2fsck -E unshare_blocks.
+ std::vector<std::string> partitions = { "/odm", "/oem", "/product", "/vendor" };
+
+ // Temporarily mount system so we can copy e2fsck_static.
+ 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");
+ }
+ if (!mounted) {
+ LOG(ERROR) << "Failed to mount system image.";
+ return false;
+ }
+ if (!copy_file(SYSTEM_E2FSCK_BIN, TMP_E2FSCK_BIN)) {
+ LOG(ERROR) << "Could not copy e2fsck to /tmp.";
+ return false;
+ }
+ if (umount("/mnt/system") < 0) {
+ PLOG(ERROR) << "umount failed";
+ return false;
+ }
+
+ bool ok = true;
+ for (const auto& partition : partitions) {
+ ok &= run_e2fsck(partition);
+ }
+
+ if (ok) {
+ LOG(INFO) << "Finished running e2fsck.";
+ } else {
+ LOG(ERROR) << "Finished running e2fsck, but not all partitions succceeded.";
+ }
+ return ok;
+}
diff --git a/mounts.h b/fsck_unshare_blocks.h
similarity index 67%
copy from mounts.h
copy to fsck_unshare_blocks.h
index 0de1ebd..9de8ef9 100644
--- a/mounts.h
+++ b/fsck_unshare_blocks.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,9 @@
* limitations under the License.
*/
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#ifndef _FILESYSTEM_CMDS_H
+#define _FILESYSTEM_CMDS_H
-struct MountedVolume;
+bool do_fsck_unshare_blocks();
-bool scan_mounted_volumes();
-
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+#endif // _FILESYSTEM_CMDS_H
diff --git a/fuse_sideload/Android.bp b/fuse_sideload/Android.bp
new file mode 100644
index 0000000..90c4c22
--- /dev/null
+++ b/fuse_sideload/Android.bp
@@ -0,0 +1,38 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_library {
+ name: "libfusesideload",
+ recovery_available: true,
+
+ cflags: [
+ "-D_XOPEN_SOURCE",
+ "-D_GNU_SOURCE",
+ "-Wall",
+ "-Werror",
+ ],
+
+ srcs: [
+ "fuse_sideload.cpp",
+ ],
+
+ export_include_dirs: [
+ "include",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libcrypto",
+ ],
+}
diff --git a/fuse_sideload.cpp b/fuse_sideload/fuse_sideload.cpp
similarity index 100%
rename from fuse_sideload.cpp
rename to fuse_sideload/fuse_sideload.cpp
diff --git a/fuse_sideload.h b/fuse_sideload/include/fuse_sideload.h
similarity index 100%
rename from fuse_sideload.h
rename to fuse_sideload/include/fuse_sideload.h
diff --git a/install.cpp b/install.cpp
index d058931..e379ef3 100644
--- a/install.cpp
+++ b/install.cpp
@@ -45,13 +45,15 @@
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
#include <vintf/VintfObjectRecovery.h>
#include <ziparchive/zip_archive.h>
#include "common.h"
-#include "otautil/SysUtil.h"
-#include "otautil/ThermalUtil.h"
#include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
+#include "otautil/thermalutil.h"
#include "private/install.h"
#include "roots.h"
#include "ui.h"
@@ -123,11 +125,9 @@
}
}
-#ifdef AB_OTA_UPDATER
-
-// Parses the metadata of the OTA package in |zip| and checks whether we are
-// allowed to accept this A/B package. Downgrading is not allowed unless
-// explicitly enabled in the package and only for incremental packages.
+// 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)) {
@@ -213,8 +213,7 @@
return 0;
}
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
- const std::string& binary_path, int /* retry_count */, int status_fd,
+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);
@@ -249,7 +248,7 @@
}
long payload_offset = payload_entry.offset;
*cmd = {
- binary_path,
+ "/system/bin/update_engine_sideload",
"--payload=file://" + package,
android::base::StringPrintf("--offset=%ld", payload_offset),
"--headers=" + std::string(payload_properties.begin(), payload_properties.end()),
@@ -258,14 +257,11 @@
return 0;
}
-#else // !AB_OTA_UPDATER
-
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
- const std::string& binary_path, int retry_count, int status_fd,
- std::vector<std::string>* cmd) {
+int SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int retry_count,
+ int status_fd, std::vector<std::string>* cmd) {
CHECK(cmd != nullptr);
- // On traditional updates we extract the update binary from the package.
+ // In non-A/B updates we extract the update binary from the package.
static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
ZipString binary_name(UPDATE_BINARY_NAME);
ZipEntry binary_entry;
@@ -274,15 +270,16 @@
return INSTALL_CORRUPT;
}
+ const std::string binary_path = Paths::Get().temporary_update_binary();
unlink(binary_path.c_str());
- int fd = open(binary_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0755);
+ android::base::unique_fd fd(
+ open(binary_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0755));
if (fd == -1) {
PLOG(ERROR) << "Failed to create " << binary_path;
return INSTALL_ERROR;
}
int32_t error = ExtractEntryToFile(zip, &binary_entry, fd);
- close(fd);
if (error != 0) {
LOG(ERROR) << "Failed to extract " << UPDATE_BINARY_NAME << ": " << ErrorCodeString(error);
return INSTALL_ERROR;
@@ -299,7 +296,6 @@
}
return 0;
}
-#endif // !AB_OTA_UPDATER
static void log_max_temperature(int* max_temperature, const std::atomic<bool>& logger_finished) {
CHECK(max_temperature != nullptr);
@@ -320,14 +316,10 @@
int pipefd[2];
pipe(pipefd);
+ bool is_ab = android::base::GetBoolProperty("ro.build.ab_update", false);
std::vector<std::string> args;
-#ifdef AB_OTA_UPDATER
- int ret = update_binary_command(package, zip, "/sbin/update_engine_sideload", retry_count,
- pipefd[1], &args);
-#else
- int ret = update_binary_command(package, zip, "/tmp/update-binary", retry_count, pipefd[1],
- &args);
-#endif
+ 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]);
@@ -627,10 +619,8 @@
return result;
}
-int install_package(const std::string& path, bool* wipe_cache, const std::string& install_file,
- bool needs_mount, int retry_count) {
+int install_package(const std::string& path, bool* wipe_cache, bool needs_mount, int retry_count) {
CHECK(!path.empty());
- CHECK(!install_file.empty());
CHECK(wipe_cache != nullptr);
modified_flash = true;
@@ -693,6 +683,7 @@
std::string log_content =
android::base::Join(log_header, "\n") + "\n" + android::base::Join(log_buffer, "\n") + "\n";
+ const std::string& install_file = Paths::Get().temporary_install_file();
if (!android::base::WriteStringToFile(log_content, install_file)) {
PLOG(ERROR) << "failed to write " << install_file;
}
diff --git a/install.h b/install.h
index f3fda30..1d3d0cd 100644
--- a/install.h
+++ b/install.h
@@ -17,16 +17,26 @@
#ifndef RECOVERY_INSTALL_H_
#define RECOVERY_INSTALL_H_
+#include <stddef.h>
+
#include <string>
+
#include <ziparchive/zip_archive.h>
-enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED,
- INSTALL_RETRY };
+enum InstallResult {
+ INSTALL_SUCCESS,
+ INSTALL_ERROR,
+ INSTALL_CORRUPT,
+ INSTALL_NONE,
+ INSTALL_SKIPPED,
+ INSTALL_RETRY,
+ INSTALL_KEY_INTERRUPTED
+};
// Installs the given update package. If INSTALL_SUCCESS is returned and *wipe_cache is true on
// exit, caller should wipe the cache partition.
-int install_package(const std::string& package, bool* wipe_cache, const std::string& install_file,
- bool needs_mount, int retry_count);
+int install_package(const std::string& package, bool* wipe_cache, bool needs_mount,
+ int retry_count);
// Verify the package by ota keys. Return true if the package is verified successfully,
// otherwise return false.
diff --git a/logging.cpp b/logging.cpp
new file mode 100644
index 0000000..50642a2
--- /dev/null
+++ b/logging.cpp
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "logging.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/klog.h>
+#include <sys/types.h>
+
+#include <string>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <private/android_filesystem_config.h> /* for AID_SYSTEM */
+#include <private/android_logger.h> /* private pmsg functions */
+
+#include "common.h"
+#include "otautil/dirutil.h"
+#include "otautil/paths.h"
+#include "roots.h"
+
+static constexpr const char* LOG_FILE = "/cache/recovery/log";
+static constexpr const char* LAST_INSTALL_FILE = "/cache/recovery/last_install";
+static constexpr const char* LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+static constexpr const char* LAST_LOG_FILE = "/cache/recovery/last_log";
+
+static const std::string LAST_KMSG_FILTER = "recovery/last_kmsg";
+static const std::string LAST_LOG_FILTER = "recovery/last_log";
+
+// fopen(3)'s the given file, by mounting volumes and making parent dirs as necessary. Returns the
+// file pointer, or nullptr on error.
+static FILE* fopen_path(const std::string& path, const char* mode) {
+ if (ensure_path_mounted(path.c_str()) != 0) {
+ LOG(ERROR) << "Can't mount " << path;
+ return nullptr;
+ }
+
+ // When writing, try to create the containing directory, if necessary. Use generous permissions,
+ // the system (init.rc) will reset them.
+ if (strchr("wa", mode[0])) {
+ mkdir_recursively(path, 0777, true, sehandle);
+ }
+ return fopen(path.c_str(), mode);
+}
+
+void check_and_fclose(FILE* fp, const std::string& name) {
+ fflush(fp);
+ if (fsync(fileno(fp)) == -1) {
+ PLOG(ERROR) << "Failed to fsync " << name;
+ }
+ if (ferror(fp)) {
+ PLOG(ERROR) << "Error in " << name;
+ }
+ fclose(fp);
+}
+
+// close a file, log an error if the error indicator is set
+ssize_t logbasename(log_id_t /* id */, char /* prio */, const char* filename, const char* /* buf */,
+ size_t len, void* arg) {
+ bool* do_rotate = static_cast<bool*>(arg);
+ if (LAST_KMSG_FILTER.find(filename) != std::string::npos ||
+ LAST_LOG_FILTER.find(filename) != std::string::npos) {
+ *do_rotate = true;
+ }
+ return len;
+}
+
+ssize_t logrotate(log_id_t id, char prio, const char* filename, const char* buf, size_t len,
+ void* arg) {
+ bool* do_rotate = static_cast<bool*>(arg);
+ if (!*do_rotate) {
+ return __android_log_pmsg_file_write(id, prio, filename, buf, len);
+ }
+
+ std::string name(filename);
+ size_t dot = name.find_last_of('.');
+ std::string sub = name.substr(0, dot);
+
+ if (LAST_KMSG_FILTER.find(sub) == std::string::npos &&
+ LAST_LOG_FILTER.find(sub) == std::string::npos) {
+ return __android_log_pmsg_file_write(id, prio, filename, buf, len);
+ }
+
+ // filename rotation
+ if (dot == std::string::npos) {
+ name += ".1";
+ } else {
+ std::string number = name.substr(dot + 1);
+ if (!isdigit(number[0])) {
+ name += ".1";
+ } else {
+ size_t i;
+ if (!android::base::ParseUint(number, &i)) {
+ LOG(ERROR) << "failed to parse uint in " << number;
+ return -1;
+ }
+ name = sub + "." + std::to_string(i + 1);
+ }
+ }
+
+ return __android_log_pmsg_file_write(id, prio, name.c_str(), buf, len);
+}
+
+// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max.
+// Similarly rename last_kmsg -> last_kmsg.1 -> ... -> last_kmsg.$max.
+// Overwrite any existing last_log.$max and last_kmsg.$max.
+void rotate_logs(const char* last_log_file, const char* last_kmsg_file) {
+ // Logs should only be rotated once.
+ static bool rotated = false;
+ if (rotated) {
+ return;
+ }
+ rotated = true;
+
+ for (int i = KEEP_LOG_COUNT - 1; i >= 0; --i) {
+ std::string old_log = android::base::StringPrintf("%s", last_log_file);
+ if (i > 0) {
+ old_log += "." + std::to_string(i);
+ }
+ std::string new_log = android::base::StringPrintf("%s.%d", last_log_file, i + 1);
+ // Ignore errors if old_log doesn't exist.
+ rename(old_log.c_str(), new_log.c_str());
+
+ std::string old_kmsg = android::base::StringPrintf("%s", last_kmsg_file);
+ if (i > 0) {
+ old_kmsg += "." + std::to_string(i);
+ }
+ std::string new_kmsg = android::base::StringPrintf("%s.%d", last_kmsg_file, i + 1);
+ rename(old_kmsg.c_str(), new_kmsg.c_str());
+ }
+}
+
+// Writes content to the current pmsg session.
+static ssize_t __pmsg_write(const std::string& filename, const std::string& buf) {
+ return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filename.c_str(),
+ buf.data(), buf.size());
+}
+
+void copy_log_file_to_pmsg(const std::string& source, const std::string& destination) {
+ std::string content;
+ android::base::ReadFileToString(source, &content);
+ __pmsg_write(destination, content);
+}
+
+// How much of the temp log we have copied to the copy in cache.
+static off_t tmplog_offset = 0;
+
+void reset_tmplog_offset() {
+ tmplog_offset = 0;
+}
+
+void copy_log_file(const std::string& source, const std::string& destination, bool append) {
+ FILE* dest_fp = fopen_path(destination, append ? "ae" : "we");
+ if (dest_fp == nullptr) {
+ PLOG(ERROR) << "Can't open " << destination;
+ } else {
+ FILE* source_fp = fopen(source.c_str(), "re");
+ if (source_fp != nullptr) {
+ if (append) {
+ fseeko(source_fp, tmplog_offset, SEEK_SET); // Since last write
+ }
+ char buf[4096];
+ size_t bytes;
+ while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
+ fwrite(buf, 1, bytes, dest_fp);
+ }
+ if (append) {
+ tmplog_offset = ftello(source_fp);
+ }
+ check_and_fclose(source_fp, source);
+ }
+ check_and_fclose(dest_fp, destination);
+ }
+}
+
+void copy_logs(bool modified_flash, bool has_cache) {
+ // We only rotate and record the log of the current session if there are actual attempts to modify
+ // the flash, such as wipes, installs from BCB or menu selections. This is to avoid unnecessary
+ // rotation (and possible deletion) of log files, if it does not do anything loggable.
+ if (!modified_flash) {
+ return;
+ }
+
+ // Always write to pmsg, this allows the OTA logs to be caught in `logcat -L`.
+ copy_log_file_to_pmsg(Paths::Get().temporary_log_file(), LAST_LOG_FILE);
+ copy_log_file_to_pmsg(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE);
+
+ // We can do nothing for now if there's no /cache partition.
+ if (!has_cache) {
+ return;
+ }
+
+ ensure_path_mounted(LAST_LOG_FILE);
+ ensure_path_mounted(LAST_KMSG_FILE);
+ rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
+
+ // Copy logs to cache so the system can find out what happened.
+ copy_log_file(Paths::Get().temporary_log_file(), LOG_FILE, true);
+ copy_log_file(Paths::Get().temporary_log_file(), LAST_LOG_FILE, false);
+ copy_log_file(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE, false);
+ save_kernel_log(LAST_KMSG_FILE);
+ chmod(LOG_FILE, 0600);
+ chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
+ chmod(LAST_KMSG_FILE, 0600);
+ chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
+ chmod(LAST_LOG_FILE, 0640);
+ chmod(LAST_INSTALL_FILE, 0644);
+ chown(LAST_INSTALL_FILE, AID_SYSTEM, AID_SYSTEM);
+ sync();
+}
+
+// Read from kernel log into buffer and write out to file.
+void save_kernel_log(const char* destination) {
+ int klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
+ if (klog_buf_len <= 0) {
+ PLOG(ERROR) << "Error getting klog size";
+ return;
+ }
+
+ std::string buffer(klog_buf_len, 0);
+ int n = klogctl(KLOG_READ_ALL, &buffer[0], klog_buf_len);
+ if (n == -1) {
+ PLOG(ERROR) << "Error in reading klog";
+ return;
+ }
+ buffer.resize(n);
+ android::base::WriteStringToFile(buffer, destination);
+}
diff --git a/rotate_logs.h b/logging.h
similarity index 70%
rename from rotate_logs.h
rename to logging.h
index 007c33d..3cfbc7a 100644
--- a/rotate_logs.h
+++ b/logging.h
@@ -14,12 +14,14 @@
* limitations under the License.
*/
-#ifndef _ROTATE_LOGS_H
-#define _ROTATE_LOGS_H
+#ifndef _LOGGING_H
+#define _LOGGING_H
#include <stddef.h>
#include <sys/types.h>
+#include <string>
+
#include <log/log_id.h>
static constexpr int KEEP_LOG_COUNT = 10;
@@ -35,4 +37,14 @@
// Overwrite any existing last_log.$max and last_kmsg.$max.
void rotate_logs(const char* last_log_file, const char* last_kmsg_file);
-#endif //_ROTATE_LOG_H
+// In turn fflush(3)'s, fsync(3)'s and fclose(3)'s the given stream.
+void check_and_fclose(FILE* fp, const std::string& name);
+
+void copy_log_file_to_pmsg(const std::string& source, const std::string& destination);
+void copy_log_file(const std::string& source, const std::string& destination, bool append);
+void copy_logs(bool modified_flash, bool has_cache);
+void reset_tmplog_offset();
+
+void save_kernel_log(const char* destination);
+
+#endif //_LOGGING_H
diff --git a/minadbd/Android.bp b/minadbd/Android.bp
new file mode 100644
index 0000000..370232b
--- /dev/null
+++ b/minadbd/Android.bp
@@ -0,0 +1,79 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+ name: "minadbd_defaults",
+
+ cflags: [
+ "-DADB_HOST=0",
+ "-Wall",
+ "-Werror",
+ ],
+
+ include_dirs: [
+ "system/core/adb",
+ ],
+}
+
+// `libminadbd_services` is analogous to the `libadbd_services` for regular `adbd`, but providing
+// the sideload service only.
+cc_library {
+ name: "libminadbd_services",
+ recovery_available: true,
+
+ defaults: [
+ "minadbd_defaults",
+ ],
+
+ srcs: [
+ "fuse_adb_provider.cpp",
+ "minadbd.cpp",
+ "minadbd_services.cpp",
+ ],
+
+ shared_libs: [
+ "libadbd",
+ "libbase",
+ "libcrypto",
+ "libfusesideload",
+ ],
+}
+
+cc_test {
+ name: "minadbd_test",
+ isolated: true,
+
+ defaults: [
+ "minadbd_defaults",
+ ],
+
+ srcs: [
+ "fuse_adb_provider_test.cpp",
+ ],
+
+ static_libs: [
+ "libminadbd_services",
+ "libadbd",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libcutils",
+ "liblog",
+ ],
+
+ test_suites: [
+ "device-tests",
+ ],
+}
diff --git a/minadbd/Android.mk b/minadbd/Android.mk
deleted file mode 100644
index 50e3b34..0000000
--- a/minadbd/Android.mk
+++ /dev/null
@@ -1,55 +0,0 @@
-# Copyright 2005 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH:= $(call my-dir)
-
-minadbd_cflags := \
- -Wall -Werror \
- -DADB_HOST=0 \
-
-# libminadbd (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
- fuse_adb_provider.cpp \
- minadbd.cpp \
- minadbd_services.cpp \
-
-LOCAL_MODULE := libminadbd
-LOCAL_CFLAGS := $(minadbd_cflags)
-LOCAL_C_INCLUDES := bootable/recovery system/core/adb
-LOCAL_WHOLE_STATIC_LIBRARIES := libadbd
-LOCAL_STATIC_LIBRARIES := libcrypto libbase
-
-include $(BUILD_STATIC_LIBRARY)
-
-# minadbd_test (native test)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := minadbd_test
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_SRC_FILES := fuse_adb_provider_test.cpp
-LOCAL_CFLAGS := $(minadbd_cflags)
-LOCAL_C_INCLUDES := $(LOCAL_PATH) system/core/adb
-LOCAL_STATIC_LIBRARIES := \
- libBionicGtestMain \
- libminadbd
-LOCAL_SHARED_LIBRARIES := \
- liblog \
- libbase \
- libcutils
-
-include $(BUILD_NATIVE_TEST)
diff --git a/minadbd/minadbd_services.cpp b/minadbd/minadbd_services.cpp
index 043c51a..e9c51da 100644
--- a/minadbd/minadbd_services.cpp
+++ b/minadbd/minadbd_services.cpp
@@ -21,46 +21,35 @@
#include <string.h>
#include <unistd.h>
+#include <functional>
#include <string>
#include <thread>
#include "adb.h"
+#include "adb_unique_fd.h"
#include "fdevent.h"
#include "fuse_adb_provider.h"
+#include "services.h"
#include "sysdeps.h"
-static void sideload_host_service(int sfd, const std::string& args) {
- int file_size;
- int block_size;
- if (sscanf(args.c_str(), "%d:%d", &file_size, &block_size) != 2) {
- printf("bad sideload-host arguments: %s\n", args.c_str());
- exit(1);
- }
+static void sideload_host_service(unique_fd sfd, const std::string& args) {
+ int64_t file_size;
+ int block_size;
+ if ((sscanf(args.c_str(), "%" SCNd64 ":%d", &file_size, &block_size) != 2) || file_size <= 0 ||
+ block_size <= 0) {
+ printf("bad sideload-host arguments: %s\n", args.c_str());
+ exit(1);
+ }
- printf("sideload-host file size %d block size %d\n", file_size, block_size);
+ printf("sideload-host file size %" PRId64 " block size %d\n", file_size, block_size);
- int result = run_adb_fuse(sfd, file_size, block_size);
+ int result = run_adb_fuse(sfd, file_size, block_size);
- printf("sideload_host finished\n");
- exit(result == 0 ? 0 : 1);
+ printf("sideload_host finished\n");
+ exit(result == 0 ? 0 : 1);
}
-static int create_service_thread(void (*func)(int, const std::string&), const std::string& args) {
- int s[2];
- if (adb_socketpair(s)) {
- printf("cannot create service socket pair\n");
- return -1;
- }
-
- std::thread([s, func, args]() { func(s[1], args); }).detach();
-
- VLOG(SERVICES) << "service thread started, " << s[0] << ":" << s[1];
- return s[0];
-}
-
-int service_to_fd(const char* name, atransport* /* transport */) {
- int ret = -1;
-
+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
@@ -68,10 +57,8 @@
exit(3);
} else if (!strncmp(name, "sideload-host:", 14)) {
std::string arg(name + 14);
- ret = create_service_thread(sideload_host_service, arg);
+ return create_service_thread("sideload-host",
+ std::bind(sideload_host_service, std::placeholders::_1, arg));
}
- if (ret >= 0) {
- close_on_exec(ret);
- }
- return ret;
+ return unique_fd{};
}
diff --git a/minui/Android.bp b/minui/Android.bp
new file mode 100644
index 0000000..fff3a8e
--- /dev/null
+++ b/minui/Android.bp
@@ -0,0 +1,47 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_library {
+ name: "libminui",
+ recovery_available: true,
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ export_include_dirs: [
+ "include",
+ ],
+
+ srcs: [
+ "events.cpp",
+ "graphics.cpp",
+ "graphics_adf.cpp",
+ "graphics_drm.cpp",
+ "graphics_fbdev.cpp",
+ "resources.cpp",
+ ],
+
+ whole_static_libs: [
+ "libadf",
+ "libdrm",
+ "libsync",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libpng",
+ "libz",
+ ],
+}
diff --git a/minui/Android.mk b/minui/Android.mk
deleted file mode 100644
index ae1552b..0000000
--- a/minui/Android.mk
+++ /dev/null
@@ -1,85 +0,0 @@
-# Copyright (C) 2007 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH := $(call my-dir)
-
-# libminui (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
- events.cpp \
- graphics.cpp \
- graphics_adf.cpp \
- graphics_drm.cpp \
- graphics_fbdev.cpp \
- resources.cpp \
-
-LOCAL_WHOLE_STATIC_LIBRARIES := \
- libadf \
- libdrm \
- libsync_recovery
-
-LOCAL_STATIC_LIBRARIES := \
- libpng \
- libbase
-
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-
-LOCAL_MODULE := libminui
-
-# This used to compare against values in double-quotes (which are just
-# ordinary characters in this context). Strip double-quotes from the
-# value so that either will work.
-
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),ABGR_8888)
- LOCAL_CFLAGS += -DRECOVERY_ABGR
-endif
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),RGBX_8888)
- LOCAL_CFLAGS += -DRECOVERY_RGBX
-endif
-ifeq ($(subst ",,$(TARGET_RECOVERY_PIXEL_FORMAT)),BGRA_8888)
- LOCAL_CFLAGS += -DRECOVERY_BGRA
-endif
-
-ifneq ($(TARGET_RECOVERY_OVERSCAN_PERCENT),)
- LOCAL_CFLAGS += -DOVERSCAN_PERCENT=$(TARGET_RECOVERY_OVERSCAN_PERCENT)
-else
- LOCAL_CFLAGS += -DOVERSCAN_PERCENT=0
-endif
-
-ifneq ($(TARGET_RECOVERY_DEFAULT_ROTATION),)
- LOCAL_CFLAGS += -DDEFAULT_ROTATION=$(TARGET_RECOVERY_DEFAULT_ROTATION)
-else
- LOCAL_CFLAGS += -DDEFAULT_ROTATION=ROTATION_NONE
-endif
-
-include $(BUILD_STATIC_LIBRARY)
-
-# libminui (shared library)
-# ===============================
-# Used by OEMs for factory test images.
-include $(CLEAR_VARS)
-LOCAL_MODULE := libminui
-LOCAL_WHOLE_STATIC_LIBRARIES += libminui
-LOCAL_SHARED_LIBRARIES := \
- libpng \
- libbase
-
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_C_INCLUDES := $(LOCAL_PATH)/include
-LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/include
-include $(BUILD_SHARED_LIBRARY)
diff --git a/minui/font_10x18.h b/minui/font_10x18.h
deleted file mode 100644
index 30dfb9c..0000000
--- a/minui/font_10x18.h
+++ /dev/null
@@ -1,214 +0,0 @@
-struct {
- unsigned width;
- unsigned height;
- unsigned char_width;
- unsigned char_height;
- unsigned char rundata[2973];
-} font = {
- .width = 960,
- .height = 18,
- .char_width = 10,
- .char_height = 18,
- .rundata = {
-0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x55,0x82,0x06,0x82,0x02,0x82,0x10,0x82,
-0x11,0x83,0x08,0x82,0x0a,0x82,0x04,0x82,0x46,0x82,0x08,0x82,0x07,0x84,0x06,
-0x84,0x0a,0x81,0x03,0x88,0x04,0x84,0x04,0x88,0x04,0x84,0x06,0x84,0x1e,0x81,
-0x0e,0x81,0x0a,0x84,0x06,0x84,0x07,0x82,0x05,0x85,0x07,0x84,0x04,0x86,0x04,
-0x88,0x02,0x88,0x04,0x84,0x04,0x82,0x04,0x82,0x02,0x88,0x05,0x86,0x01,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,
-0x86,0x06,0x84,0x04,0x86,0x06,0x84,0x04,0x88,0x02,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
-0x88,0x03,0x86,0x0e,0x86,0x06,0x82,0x11,0x82,0x10,0x82,0x18,0x82,0x0f,0x84,
-0x0d,0x82,0x1c,0x82,0x09,0x84,0x7f,0x16,0x84,0x05,0x82,0x05,0x84,0x07,0x83,
-0x02,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x03,0x86,0x04,
-0x83,0x02,0x82,0x03,0x82,0x01,0x82,0x07,0x82,0x09,0x82,0x06,0x82,0x3e,0x82,
-0x04,0x84,0x06,0x83,0x06,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x03,
-0x82,0x09,0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
-0x1c,0x82,0x0e,0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x05,0x84,0x04,
-0x82,0x02,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
-0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x04,
-0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x02,
-0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x03,0x82,0x08,0x82,0x0c,
-0x82,0x05,0x84,0x11,0x82,0x0f,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,
-0x1c,0x82,0x0b,0x82,0x7f,0x15,0x82,0x08,0x82,0x08,0x82,0x05,0x82,0x01,0x82,
-0x01,0x82,0x19,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x01,0x82,0x02,0x82,0x01,
-0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x07,0x82,
-0x08,0x82,0x08,0x82,0x3d,0x82,0x03,0x82,0x02,0x82,0x04,0x84,0x05,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x06,0x83,0x03,0x82,0x08,0x82,0x04,0x81,0x09,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x1a,0x82,0x10,0x82,0x06,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,
-0x02,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x09,0x82,0x03,0x82,0x08,0x82,0x0c,0x82,0x04,0x82,0x02,0x82,
-0x11,0x82,0x0e,0x82,0x18,0x82,0x0e,0x82,0x02,0x82,0x0c,0x82,0x0b,0x82,0x0b,
-0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x05,0x82,
-0x02,0x83,0x1a,0x82,0x07,0x81,0x02,0x81,0x07,0x82,0x01,0x82,0x02,0x82,0x01,
-0x82,0x05,0x82,0x01,0x84,0x04,0x82,0x01,0x82,0x07,0x82,0x08,0x82,0x08,0x82,
-0x06,0x82,0x02,0x82,0x06,0x82,0x28,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,
-0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x84,0x03,0x82,0x08,0x82,
-0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x19,0x82,0x12,0x82,0x05,
-0x82,0x04,0x82,0x02,0x82,0x02,0x84,0x03,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
-0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,
-0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x83,0x02,0x83,
-0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,0x05,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
-0x82,0x04,0x82,0x09,0x82,0x0b,0x82,0x03,0x82,0x04,0x82,0x20,0x82,0x18,0x82,
-0x0e,0x82,0x10,0x82,0x0b,0x82,0x0b,0x82,0x02,0x82,0x0b,0x82,0x4d,0x82,0x45,
-0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x88,0x01,0x82,0x01,0x82,0x06,0x83,
-0x01,0x82,0x04,0x84,0x08,0x81,0x08,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,
-0x82,0x28,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x08,0x82,0x04,0x82,
-0x01,0x82,0x03,0x82,0x08,0x82,0x0d,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x18,0x82,0x06,0x88,0x06,0x82,0x04,0x82,0x04,0x82,0x02,0x82,0x01,0x85,
-0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,
-0x02,0x82,0x04,0x82,0x08,0x88,0x02,0x84,0x02,0x82,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x0b,0x82,
-0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x84,0x06,
-0x84,0x08,0x82,0x05,0x82,0x09,0x82,0x0b,0x82,0x2b,0x82,0x18,0x82,0x0e,0x82,
-0x10,0x82,0x1c,0x82,0x0b,0x82,0x4d,0x82,0x45,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x11,0x82,0x01,0x82,0x03,0x82,0x01,0x82,0x09,0x82,0x06,0x82,0x12,0x82,
-0x0a,0x82,0x06,0x84,0x07,0x82,0x27,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,
-0x82,0x07,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x01,0x83,0x04,0x82,0x01,0x83,
-0x08,0x82,0x05,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x05,0x83,0x07,0x83,0x05,
-0x82,0x16,0x82,0x08,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,
-0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,
-0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
-0x0a,0x82,0x05,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,
-0x82,0x04,0x84,0x06,0x84,0x08,0x82,0x05,0x82,0x0a,0x82,0x0a,0x82,0x23,0x85,
-0x03,0x82,0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x84,0x04,0x86,0x05,
-0x85,0x01,0x81,0x02,0x82,0x01,0x83,0x05,0x84,0x09,0x84,0x02,0x82,0x03,0x82,
-0x06,0x82,0x05,0x81,0x01,0x82,0x01,0x82,0x03,0x82,0x01,0x83,0x06,0x84,0x04,
-0x82,0x01,0x83,0x06,0x83,0x01,0x82,0x02,0x82,0x01,0x84,0x04,0x86,0x03,0x86,
-0x04,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x87,0x05,0x82,0x08,0x82,0x08,0x82,0x26,0x82,
-0x11,0x82,0x01,0x82,0x04,0x86,0x07,0x82,0x05,0x83,0x12,0x82,0x0a,0x82,0x04,
-0x88,0x02,0x88,0x0c,0x88,0x10,0x82,0x04,0x82,0x04,0x82,0x05,0x82,0x0a,0x82,
-0x06,0x83,0x04,0x82,0x03,0x82,0x03,0x83,0x02,0x82,0x03,0x83,0x02,0x82,0x07,
-0x82,0x06,0x84,0x05,0x82,0x02,0x83,0x05,0x83,0x07,0x83,0x04,0x82,0x18,0x82,
-0x06,0x82,0x04,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x86,0x04,
-0x82,0x08,0x82,0x04,0x82,0x02,0x86,0x04,0x86,0x04,0x82,0x02,0x84,0x02,0x88,
-0x05,0x82,0x0a,0x82,0x03,0x85,0x05,0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,
-0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x02,0x82,0x03,0x82,0x05,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x03,
-0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x08,0x82,0x08,0x82,
-0x06,0x82,0x0a,0x82,0x0a,0x82,0x22,0x82,0x03,0x82,0x02,0x83,0x02,0x82,0x04,
-0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x02,0x82,0x05,0x82,0x06,0x82,
-0x03,0x83,0x02,0x83,0x02,0x82,0x06,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,0x07,
-0x82,0x05,0x88,0x02,0x83,0x02,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,
-0x04,0x82,0x02,0x83,0x03,0x83,0x02,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,
-0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x04,0x82,0x08,0x82,0x02,0x84,0x09,0x82,0x09,0x84,0x23,0x82,0x11,
-0x82,0x01,0x82,0x06,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x01,0x82,0x11,0x82,
-0x0a,0x82,0x06,0x84,0x07,0x82,0x26,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x08,
-0x83,0x09,0x82,0x03,0x82,0x03,0x82,0x09,0x82,0x02,0x82,0x04,0x82,0x05,0x82,
-0x06,0x82,0x02,0x82,0x05,0x83,0x01,0x82,0x17,0x82,0x16,0x82,0x06,0x82,0x05,
-0x82,0x01,0x82,0x01,0x82,0x02,0x88,0x02,0x82,0x03,0x82,0x03,0x82,0x08,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
-0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x86,0x04,0x82,0x04,0x82,0x02,
-0x86,0x09,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,
-0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,0x82,0x27,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
-0x82,0x01,0x82,0x08,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x07,
-0x82,0x0a,0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x04,0x82,
-0x04,0x84,0x04,0x82,0x04,0x82,0x07,0x82,0x06,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x0f,0x88,0x05,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x02,0x82,0x01,0x82,
-0x0d,0x82,0x0a,0x82,0x05,0x82,0x02,0x82,0x06,0x82,0x26,0x82,0x05,0x82,0x04,
-0x82,0x05,0x82,0x07,0x82,0x0c,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,
-0x05,0x82,0x05,0x82,0x04,0x82,0x08,0x82,0x18,0x82,0x14,0x82,0x07,0x82,0x05,
-0x82,0x01,0x84,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x0a,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x08,0x82,0x01,0x82,0x01,0x82,
-0x02,0x82,0x02,0x84,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,
-0x82,0x02,0x82,0x0a,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x82,
-0x01,0x82,0x01,0x82,0x04,0x84,0x07,0x82,0x07,0x82,0x07,0x82,0x0b,0x82,0x09,
-0x82,0x22,0x87,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x88,
-0x04,0x82,0x06,0x82,0x03,0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,
-0x84,0x09,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x08,0x86,0x05,
-0x82,0x06,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x03,0x82,0x01,0x82,0x01,0x82,
-0x05,0x82,0x05,0x82,0x04,0x82,0x06,0x82,0x07,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x10,0x82,0x01,0x82,0x07,0x82,0x01,0x82,0x04,0x82,0x01,0x83,0x02,0x82,
-0x03,0x83,0x0f,0x82,0x08,0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x25,0x82,0x07,
-0x82,0x02,0x82,0x06,0x82,0x06,0x82,0x07,0x82,0x04,0x82,0x07,0x82,0x09,0x82,
-0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,0x82,0x08,0x82,0x19,0x82,0x05,
-0x88,0x05,0x82,0x08,0x82,0x05,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x08,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,0x03,0x82,0x03,0x82,
-0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,
-0x82,0x08,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x03,0x82,0x09,0x82,0x05,0x82,
-0x05,0x82,0x04,0x82,0x04,0x84,0x04,0x83,0x02,0x83,0x03,0x82,0x02,0x82,0x06,
-0x82,0x06,0x82,0x08,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,0x04,0x82,0x02,0x82,
-0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,0x82,0x06,0x82,0x03,
-0x82,0x03,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x85,0x08,0x82,0x05,0x82,
-0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,0x06,0x82,0x04,0x82,
-0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,
-0x82,0x08,0x82,0x08,0x82,0x08,0x82,0x38,0x82,0x01,0x82,0x04,0x82,0x01,0x82,
-0x01,0x82,0x04,0x84,0x01,0x82,0x01,0x82,0x03,0x82,0x10,0x82,0x08,0x82,0x30,
-0x83,0x06,0x82,0x07,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x08,0x82,0x04,0x82,
-0x07,0x82,0x03,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x04,0x82,0x06,0x82,0x04,
-0x82,0x03,0x81,0x04,0x82,0x1a,0x82,0x10,0x82,0x10,0x82,0x08,0x82,0x04,0x82,
-0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,
-0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,0x82,0x05,0x82,0x03,0x82,
-0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x03,0x83,0x02,
-0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x02,0x84,0x02,0x82,0x03,0x82,0x03,0x82,
-0x04,0x82,0x05,0x82,0x05,0x82,0x04,0x82,0x05,0x82,0x05,0x83,0x02,0x83,0x03,
-0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x09,0x82,0x0c,0x82,0x08,0x82,0x21,0x82,
-0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x0a,
-0x82,0x07,0x85,0x04,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x02,0x82,
-0x07,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x0d,0x82,0x04,0x82,
-0x06,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x82,0x01,0x82,0x04,0x84,0x04,
-0x82,0x04,0x82,0x04,0x82,0x09,0x82,0x08,0x82,0x08,0x82,0x26,0x82,0x10,0x82,
-0x01,0x82,0x05,0x86,0x04,0x82,0x01,0x82,0x01,0x82,0x01,0x83,0x01,0x84,0x10,
-0x82,0x06,0x82,0x1d,0x83,0x11,0x83,0x05,0x82,0x09,0x84,0x07,0x82,0x05,0x82,
-0x09,0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x04,
-0x82,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x06,0x83,0x07,0x83,0x09,0x82,
-0x0e,0x82,0x0a,0x82,0x06,0x82,0x03,0x82,0x02,0x82,0x04,0x82,0x02,0x82,0x03,
-0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x03,0x82,0x03,0x82,0x08,0x82,0x09,0x82,
-0x02,0x83,0x02,0x82,0x04,0x82,0x05,0x82,0x06,0x82,0x01,0x82,0x04,0x82,0x04,
-0x82,0x02,0x82,0x08,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x03,0x82,0x02,0x82,
-0x03,0x82,0x09,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x03,0x82,0x02,0x82,0x06,
-0x82,0x06,0x82,0x02,0x82,0x06,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,
-0x05,0x82,0x05,0x82,0x09,0x82,0x0d,0x82,0x07,0x82,0x21,0x82,0x04,0x82,0x02,
-0x83,0x02,0x82,0x04,0x82,0x03,0x82,0x03,0x82,0x02,0x83,0x03,0x82,0x03,0x82,
-0x04,0x82,0x06,0x82,0x08,0x82,0x04,0x82,0x05,0x82,0x0b,0x82,0x02,0x82,0x03,
-0x82,0x06,0x82,0x05,0x82,0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x03,0x82,
-0x02,0x82,0x03,0x83,0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x07,0x82,0x04,
-0x82,0x04,0x82,0x02,0x82,0x03,0x82,0x02,0x83,0x05,0x82,0x05,0x88,0x03,0x82,
-0x02,0x82,0x04,0x82,0x02,0x83,0x03,0x82,0x0a,0x82,0x08,0x82,0x08,0x82,0x26,
-0x82,0x1c,0x82,0x06,0x82,0x02,0x83,0x03,0x84,0x02,0x82,0x10,0x82,0x04,0x82,
-0x1e,0x83,0x11,0x83,0x05,0x82,0x0a,0x82,0x05,0x88,0x02,0x88,0x04,0x84,0x09,
-0x82,0x05,0x84,0x06,0x84,0x05,0x82,0x09,0x84,0x06,0x84,0x07,0x83,0x07,0x83,
-0x0a,0x81,0x0e,0x81,0x0b,0x82,0x07,0x85,0x03,0x82,0x04,0x82,0x02,0x86,0x06,
-0x84,0x04,0x86,0x04,0x88,0x02,0x82,0x0a,0x84,0x01,0x81,0x02,0x82,0x04,0x82,
-0x02,0x88,0x04,0x83,0x05,0x82,0x04,0x82,0x02,0x88,0x02,0x82,0x04,0x82,0x02,
-0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x0a,0x85,0x03,0x82,0x04,0x82,0x04,0x84,
-0x07,0x82,0x07,0x84,0x07,0x82,0x05,0x82,0x04,0x82,0x02,0x82,0x04,0x82,0x05,
-0x82,0x05,0x88,0x03,0x86,0x09,0x82,0x03,0x86,0x22,0x85,0x01,0x81,0x02,0x82,
-0x01,0x83,0x06,0x85,0x05,0x83,0x01,0x82,0x04,0x85,0x05,0x82,0x07,0x86,0x03,
-0x82,0x04,0x82,0x02,0x88,0x08,0x82,0x02,0x82,0x04,0x82,0x02,0x88,0x02,0x82,
-0x01,0x82,0x01,0x82,0x02,0x82,0x04,0x82,0x04,0x84,0x04,0x82,0x01,0x83,0x06,
-0x83,0x01,0x82,0x03,0x82,0x08,0x86,0x06,0x84,0x05,0x83,0x01,0x82,0x05,0x82,
-0x06,0x82,0x02,0x82,0x03,0x82,0x04,0x82,0x04,0x83,0x01,0x82,0x03,0x87,0x06,
-0x84,0x05,0x82,0x05,0x84,0x7f,0x15,0x83,0x7f,0x14,0x83,0x7f,0x5e,0x82,0x7f,
-0x05,0x89,0x47,0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x4e,
-0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,0x82,0x04,0x82,0x17,0x82,0x03,0x82,
-0x34,0x82,0x0e,0x82,0x48,0x82,0x04,0x82,0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0a,
-0x82,0x04,0x82,0x17,0x82,0x03,0x82,0x34,0x82,0x0e,0x82,0x49,0x82,0x02,0x82,
-0x7f,0x7f,0x7f,0x7f,0x7f,0x7f,0x0c,0x86,0x19,0x85,0x35,0x82,0x0e,0x82,0x4a,
-0x84,0x3f,
-0x00,
- }
-};
diff --git a/minui/graphics.cpp b/minui/graphics.cpp
index 56f471b..e6367d9 100644
--- a/minui/graphics.cpp
+++ b/minui/graphics.cpp
@@ -23,41 +23,57 @@
#include <memory>
-#include "font_10x18.h"
+#include <android-base/properties.h>
+
#include "graphics_adf.h"
#include "graphics_drm.h"
#include "graphics_fbdev.h"
#include "minui/minui.h"
-static GRFont* gr_font = NULL;
+static GRFont* gr_font = nullptr;
static MinuiBackend* gr_backend = nullptr;
-static int overscan_percent = OVERSCAN_PERCENT;
static int overscan_offset_x = 0;
static int overscan_offset_y = 0;
static uint32_t gr_current = ~0;
static constexpr uint32_t alpha_mask = 0xff000000;
-static GRSurface* gr_draw = NULL;
-static GRRotation rotation = ROTATION_NONE;
+// gr_draw is owned by backends.
+static const GRSurface* gr_draw = nullptr;
+static GRRotation rotation = GRRotation::NONE;
+static PixelFormat pixel_format = PixelFormat::UNKNOWN;
static bool outside(int x, int y) {
- return x < 0 || x >= (rotation % 2 ? gr_draw->height : gr_draw->width) || y < 0 ||
- y >= (rotation % 2 ? gr_draw->width : gr_draw->height);
+ auto swapped = (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT);
+ return x < 0 || x >= (swapped ? gr_draw->height : gr_draw->width) || y < 0 ||
+ y >= (swapped ? gr_draw->width : gr_draw->height);
}
const GRFont* gr_sys_font() {
return gr_font;
}
+PixelFormat gr_pixel_format() {
+ return pixel_format;
+}
+
int gr_measure(const GRFont* font, const char* s) {
+ if (font == nullptr) {
+ return -1;
+ }
+
return font->char_width * strlen(s);
}
-void gr_font_size(const GRFont* font, int* x, int* y) {
+int gr_font_size(const GRFont* font, int* x, int* y) {
+ if (font == nullptr) {
+ return -1;
+ }
+
*x = font->char_width;
*y = font->char_height;
+ return 0;
}
// Blends gr_current onto pix value, assumes alpha as most significant byte.
@@ -78,38 +94,46 @@
return (out_r & 0xff) | (out_g & 0xff00) | (out_b & 0xff0000) | (gr_current & 0xff000000);
}
-// increments pixel pointer right, with current rotation.
+// Increments pixel pointer right, with current rotation.
static void incr_x(uint32_t** p, int row_pixels) {
- if (rotation % 2) {
- *p = *p + (rotation == 1 ? 1 : -1) * row_pixels;
- } else {
- *p = *p + (rotation ? -1 : 1);
+ if (rotation == GRRotation::LEFT) {
+ *p = *p - row_pixels;
+ } else if (rotation == GRRotation::RIGHT) {
+ *p = *p + row_pixels;
+ } else if (rotation == GRRotation::DOWN) {
+ *p = *p - 1;
+ } else { // GRRotation::NONE
+ *p = *p + 1;
}
}
-// increments pixel pointer down, with current rotation.
+// Increments pixel pointer down, with current rotation.
static void incr_y(uint32_t** p, int row_pixels) {
- if (rotation % 2) {
- *p = *p + (rotation == 1 ? -1 : 1);
- } else {
- *p = *p + (rotation ? -1 : 1) * row_pixels;
+ if (rotation == GRRotation::LEFT) {
+ *p = *p + 1;
+ } else if (rotation == GRRotation::RIGHT) {
+ *p = *p - 1;
+ } else if (rotation == GRRotation::DOWN) {
+ *p = *p - row_pixels;
+ } else { // GRRotation::NONE
+ *p = *p + row_pixels;
}
}
-// returns pixel pointer at given coordinates with rotation adjustment.
-static uint32_t* pixel_at(GRSurface* surf, int x, int y, int row_pixels) {
+// Returns pixel pointer at given coordinates with rotation adjustment.
+static uint32_t* pixel_at(const GRSurface* surf, int x, int y, int row_pixels) {
switch (rotation) {
- case ROTATION_NONE:
+ case GRRotation::NONE:
return reinterpret_cast<uint32_t*>(surf->data) + y * row_pixels + x;
- case ROTATION_RIGHT:
+ case GRRotation::RIGHT:
return reinterpret_cast<uint32_t*>(surf->data) + x * row_pixels + (surf->width - y);
- case ROTATION_DOWN:
+ case GRRotation::DOWN:
return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - y) * row_pixels +
(surf->width - 1 - x);
- case ROTATION_LEFT:
+ case GRRotation::LEFT:
return reinterpret_cast<uint32_t*>(surf->data) + (surf->height - 1 - x) * row_pixels + y;
default:
- printf("invalid rotation %d", rotation);
+ printf("invalid rotation %d", static_cast<int>(rotation));
}
return nullptr;
}
@@ -164,7 +188,7 @@
}
void gr_texticon(int x, int y, GRSurface* icon) {
- if (icon == NULL) return;
+ if (icon == nullptr) return;
if (icon->pixel_bytes != 1) {
printf("gr_texticon: source has wrong format\n");
@@ -185,11 +209,11 @@
void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a) {
uint32_t r32 = r, g32 = g, b32 = b, a32 = a;
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
- gr_current = (a32 << 24) | (r32 << 16) | (g32 << 8) | b32;
-#else
- gr_current = (a32 << 24) | (b32 << 16) | (g32 << 8) | r32;
-#endif
+ if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+ gr_current = (a32 << 24) | (r32 << 16) | (g32 << 8) | b32;
+ } else {
+ gr_current = (a32 << 24) | (b32 << 16) | (g32 << 8) | r32;
+ }
}
void gr_clear() {
@@ -235,7 +259,7 @@
}
void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy) {
- if (source == NULL) return;
+ if (source == nullptr) return;
if (gr_draw->pixel_bytes != source->pixel_bytes) {
printf("gr_blit: source has wrong format\n");
@@ -247,7 +271,7 @@
if (outside(dx, dy) || outside(dx + w - 1, dy + h - 1)) return;
- if (rotation) {
+ if (rotation != GRRotation::NONE) {
int src_row_pixels = source->row_bytes / source->pixel_bytes;
int row_pixels = gr_draw->row_bytes / gr_draw->pixel_bytes;
uint32_t* src_py = reinterpret_cast<uint32_t*>(source->data) + sy * source->row_bytes / 4 + sx;
@@ -267,8 +291,7 @@
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;
- int i;
- for (i = 0; i < h; ++i) {
+ for (int i = 0; i < h; ++i) {
memcpy(dst_p, src_p, w * source->pixel_bytes);
src_p += source->row_bytes;
dst_p += gr_draw->row_bytes;
@@ -276,15 +299,15 @@
}
}
-unsigned int gr_get_width(GRSurface* surface) {
- if (surface == NULL) {
+unsigned int gr_get_width(const GRSurface* surface) {
+ if (surface == nullptr) {
return 0;
}
return surface->width;
}
-unsigned int gr_get_height(GRSurface* surface) {
- if (surface == NULL) {
+unsigned int gr_get_height(const GRSurface* surface) {
+ if (surface == nullptr) {
return 0;
}
return surface->height;
@@ -313,42 +336,28 @@
return 0;
}
-static void gr_init_font(void) {
- int res = gr_init_font("font", &gr_font);
- if (res == 0) {
- return;
- }
-
- printf("failed to read font: res=%d\n", res);
-
- // fall back to the compiled-in font.
- gr_font = static_cast<GRFont*>(calloc(1, sizeof(*gr_font)));
- gr_font->texture = static_cast<GRSurface*>(malloc(sizeof(*gr_font->texture)));
- gr_font->texture->width = font.width;
- gr_font->texture->height = font.height;
- gr_font->texture->row_bytes = font.width;
- gr_font->texture->pixel_bytes = 1;
-
- unsigned char* bits = static_cast<unsigned char*>(malloc(font.width * font.height));
- gr_font->texture->data = bits;
-
- unsigned char data;
- unsigned char* in = font.rundata;
- while ((data = *in++)) {
- memset(bits, (data & 0x80) ? 255 : 0, data & 0x7f);
- bits += (data & 0x7f);
- }
-
- gr_font->char_width = font.char_width;
- gr_font->char_height = font.char_height;
-}
-
void gr_flip() {
gr_draw = gr_backend->Flip();
}
int gr_init() {
- gr_init_font();
+ // pixel_format needs to be set before loading any resources or initializing backends.
+ std::string format = android::base::GetProperty("ro.minui.pixel_format", "");
+ if (format == "ABGR_8888") {
+ pixel_format = PixelFormat::ABGR;
+ } else if (format == "RGBX_8888") {
+ pixel_format = PixelFormat::RGBX;
+ } else if (format == "BGRA_8888") {
+ pixel_format = PixelFormat::BGRA;
+ } else {
+ pixel_format = PixelFormat::UNKNOWN;
+ }
+
+ int ret = gr_init_font("font", &gr_font);
+ if (ret != 0) {
+ printf("Failed to init font: %d, continuing graphic backend initialization without font file\n",
+ ret);
+ }
auto backend = std::unique_ptr<MinuiBackend>{ std::make_unique<MinuiBackendAdf>() };
gr_draw = backend->Init();
@@ -369,13 +378,28 @@
gr_backend = backend.release();
+ int overscan_percent = android::base::GetIntProperty("ro.minui.overscan_percent", 0);
overscan_offset_x = gr_draw->width * overscan_percent / 100;
overscan_offset_y = gr_draw->height * overscan_percent / 100;
gr_flip();
gr_flip();
+ if (!gr_draw) {
+ printf("gr_init: gr_draw becomes nullptr after gr_flip\n");
+ return -1;
+ }
- gr_rotate(DEFAULT_ROTATION);
+ std::string rotation_str =
+ android::base::GetProperty("ro.minui.default_rotation", "ROTATION_NONE");
+ if (rotation_str == "ROTATION_RIGHT") {
+ gr_rotate(GRRotation::RIGHT);
+ } else if (rotation_str == "ROTATION_DOWN") {
+ gr_rotate(GRRotation::DOWN);
+ } else if (rotation_str == "ROTATION_LEFT") {
+ gr_rotate(GRRotation::LEFT);
+ } else { // "ROTATION_NONE" or unknown string
+ gr_rotate(GRRotation::NONE);
+ }
if (gr_draw->pixel_bytes != 4) {
printf("gr_init: Only 4-byte pixel formats supported\n");
@@ -386,16 +410,22 @@
void gr_exit() {
delete gr_backend;
+ gr_backend = nullptr;
+
+ delete gr_font;
+ gr_font = nullptr;
}
int gr_fb_width() {
- return rotation % 2 ? gr_draw->height - 2 * overscan_offset_y
- : gr_draw->width - 2 * overscan_offset_x;
+ return (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT)
+ ? gr_draw->height - 2 * overscan_offset_y
+ : gr_draw->width - 2 * overscan_offset_x;
}
int gr_fb_height() {
- return rotation % 2 ? gr_draw->width - 2 * overscan_offset_x
- : gr_draw->height - 2 * overscan_offset_y;
+ return (rotation == GRRotation::LEFT || rotation == GRRotation::RIGHT)
+ ? gr_draw->width - 2 * overscan_offset_x
+ : gr_draw->height - 2 * overscan_offset_y;
}
void gr_fb_blank(bool blank) {
diff --git a/minui/graphics_adf.cpp b/minui/graphics_adf.cpp
index a59df00..7439df9 100644
--- a/minui/graphics_adf.cpp
+++ b/minui/graphics_adf.cpp
@@ -104,15 +104,16 @@
}
GRSurface* MinuiBackendAdf::Init() {
-#if defined(RECOVERY_ABGR)
- format = DRM_FORMAT_ABGR8888;
-#elif defined(RECOVERY_BGRA)
- format = DRM_FORMAT_BGRA8888;
-#elif defined(RECOVERY_RGBX)
- format = DRM_FORMAT_RGBX8888;
-#else
- format = DRM_FORMAT_RGB565;
-#endif
+ PixelFormat pixel_format = gr_pixel_format();
+ if (pixel_format == PixelFormat::ABGR) {
+ format = DRM_FORMAT_ABGR8888;
+ } else if (pixel_format == PixelFormat::BGRA) {
+ format = DRM_FORMAT_BGRA8888;
+ } else if (pixel_format == PixelFormat::RGBX) {
+ format = DRM_FORMAT_RGBX8888;
+ } else {
+ format = DRM_FORMAT_RGB565;
+ }
adf_id_t* dev_ids = nullptr;
ssize_t n_dev_ids = adf_devices(&dev_ids);
diff --git a/minui/graphics_drm.cpp b/minui/graphics_drm.cpp
index e7d4b38..630b801 100644
--- a/minui/graphics_drm.cpp
+++ b/minui/graphics_drm.cpp
@@ -17,6 +17,7 @@
#include "graphics_drm.h"
#include <fcntl.h>
+#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
@@ -45,15 +46,17 @@
}
}
-void MinuiBackendDrm::DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface) {
- int32_t ret = drmModeSetCrtc(drm_fd, crtc->crtc_id, surface->fb_id, 0, 0, // x,y
- &main_monitor_connector->connector_id,
- 1, // connector_count
- &main_monitor_crtc->mode);
+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) {
@@ -113,15 +116,20 @@
*surface = {};
uint32_t format;
-#if defined(RECOVERY_ABGR)
- format = DRM_FORMAT_RGBA8888;
-#elif defined(RECOVERY_BGRA)
- format = DRM_FORMAT_ARGB8888;
-#elif defined(RECOVERY_RGBX)
- format = DRM_FORMAT_XBGR8888;
-#else
- format = DRM_FORMAT_RGB565;
-#endif
+ PixelFormat pixel_format = gr_pixel_format();
+ // PixelFormat comes in byte order, whereas DRM_FORMAT_* uses little-endian
+ // (external/libdrm/include/drm/drm_fourcc.h). Note that although drm_fourcc.h also defines a
+ // macro of DRM_FORMAT_BIG_ENDIAN, it doesn't seem to be actually supported (see the discussion
+ // in https://lists.freedesktop.org/archives/amd-gfx/2017-May/008560.html).
+ if (pixel_format == PixelFormat::ABGR) {
+ format = DRM_FORMAT_RGBA8888;
+ } else if (pixel_format == PixelFormat::BGRA) {
+ format = DRM_FORMAT_ARGB8888;
+ } else if (pixel_format == PixelFormat::RGBX) {
+ format = DRM_FORMAT_XBGR8888;
+ } else {
+ format = DRM_FORMAT_RGB565;
+ }
drm_mode_create_dumb create_dumb = {};
create_dumb.height = height;
@@ -368,18 +376,57 @@
current_buffer = 0;
- DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]);
+ // We will likely encounter errors in the backend functions (i.e. Flip) if EnableCrtc fails.
+ if (DrmEnableCrtc(drm_fd, main_monitor_crtc, GRSurfaceDrms[1]) != 0) {
+ return nullptr;
+ }
return GRSurfaceDrms[0];
}
+static void page_flip_complete(__unused int fd,
+ __unused unsigned int sequence,
+ __unused unsigned int tv_sec,
+ __unused unsigned int tv_usec,
+ void *user_data) {
+ *static_cast<bool*>(user_data) = false;
+}
+
GRSurface* MinuiBackendDrm::Flip() {
+ bool ongoing_flip = true;
+
int ret = drmModePageFlip(drm_fd, main_monitor_crtc->crtc_id,
- GRSurfaceDrms[current_buffer]->fb_id, 0, nullptr);
+ GRSurfaceDrms[current_buffer]->fb_id,
+ DRM_MODE_PAGE_FLIP_EVENT, &ongoing_flip);
if (ret < 0) {
printf("drmModePageFlip failed ret=%d\n", ret);
return nullptr;
}
+
+ while (ongoing_flip) {
+ struct pollfd fds = {
+ .fd = drm_fd,
+ .events = POLLIN
+ };
+
+ ret = poll(&fds, 1, -1);
+ if (ret == -1 || !(fds.revents & POLLIN)) {
+ printf("poll() failed on drm fd\n");
+ break;
+ }
+
+ drmEventContext evctx = {
+ .version = DRM_EVENT_CONTEXT_VERSION,
+ .page_flip_handler = page_flip_complete
+ };
+
+ ret = drmHandleEvent(drm_fd, &evctx);
+ if (ret != 0) {
+ printf("drmHandleEvent failed ret=%d\n", ret);
+ break;
+ }
+ }
+
current_buffer = 1 - current_buffer;
return GRSurfaceDrms[current_buffer];
}
diff --git a/minui/graphics_drm.h b/minui/graphics_drm.h
index de96212..756625b 100644
--- a/minui/graphics_drm.h
+++ b/minui/graphics_drm.h
@@ -42,7 +42,7 @@
private:
void DrmDisableCrtc(int drm_fd, drmModeCrtc* crtc);
- void DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface);
+ int DrmEnableCrtc(int drm_fd, drmModeCrtc* crtc, GRSurfaceDrm* surface);
GRSurfaceDrm* DrmCreateSurface(int width, int height);
void DrmDestroySurface(GRSurfaceDrm* surface);
void DisableNonMainCrtcs(int fd, drmModeRes* resources, drmModeCrtc* main_crtc);
diff --git a/minui/include/minui/minui.h b/minui/include/minui/minui.h
index f9da199..fa13ecd 100644
--- a/minui/include/minui/minui.h
+++ b/minui/include/minui/minui.h
@@ -41,14 +41,27 @@
int char_height;
};
-enum GRRotation {
- ROTATION_NONE = 0,
- ROTATION_RIGHT = 1,
- ROTATION_DOWN = 2,
- ROTATION_LEFT = 3,
+enum class GRRotation : int {
+ NONE = 0,
+ RIGHT = 1,
+ DOWN = 2,
+ LEFT = 3,
};
+enum class PixelFormat : int {
+ UNKNOWN = 0,
+ ABGR = 1,
+ RGBX = 2,
+ BGRA = 3,
+};
+
+// Initializes the graphics backend and loads font file. Returns 0 on success, or -1 on error. Note
+// that the font initialization failure would be non-fatal, as caller may not need to draw any text
+// at all. Caller can check the font initialization result via gr_sys_font() as needed.
int gr_init();
+
+// Frees the allocated resources. The function is idempotent, and safe to be called if gr_init()
+// didn't finish successfully.
void gr_exit();
int gr_fb_width();
@@ -57,7 +70,8 @@
void gr_flip();
void gr_fb_blank(bool blank);
-void gr_clear(); // clear entire surface to current color
+// Clears entire surface to current color.
+void gr_clear();
void gr_color(unsigned char r, unsigned char g, unsigned char b, unsigned char a);
void gr_fill(int x1, int y1, int x2, int y2);
@@ -66,16 +80,21 @@
const GRFont* gr_sys_font();
int gr_init_font(const char* name, GRFont** dest);
void gr_text(const GRFont* font, int x, int y, const char* s, bool bold);
+// Returns -1 if font is nullptr.
int gr_measure(const GRFont* font, const char* s);
-void gr_font_size(const GRFont* font, int* x, int* y);
+// Returns -1 if font is nullptr.
+int gr_font_size(const GRFont* font, int* x, int* y);
void gr_blit(GRSurface* source, int sx, int sy, int w, int h, int dx, int dy);
-unsigned int gr_get_width(GRSurface* surface);
-unsigned int gr_get_height(GRSurface* surface);
+unsigned int gr_get_width(const GRSurface* surface);
+unsigned int gr_get_height(const GRSurface* surface);
-// Set rotation, flips gr_fb_width/height if 90 degree rotation difference
+// Sets rotation, flips gr_fb_width/height if 90 degree rotation difference
void gr_rotate(GRRotation rotation);
+// Returns the current PixelFormat being used.
+PixelFormat gr_pixel_format();
+
//
// Input events.
//
diff --git a/minui/include/private/resources.h b/minui/include/private/resources.h
new file mode 100644
index 0000000..047ebe2
--- /dev/null
+++ b/minui/include/private/resources.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdio.h>
+
+#include <memory>
+#include <string>
+
+#include <png.h>
+
+// This class handles the PNG file parsing. It also holds the ownership of the PNG pointer and the
+// opened file pointer. Both will be destroyed / closed when this object goes out of scope.
+class PngHandler {
+ public:
+ // Constructs an instance by loading the PNG file from '/res/images/<name>.png', or '<name>'.
+ PngHandler(const std::string& name);
+
+ ~PngHandler();
+
+ png_uint_32 width() const {
+ return width_;
+ }
+
+ png_uint_32 height() const {
+ return height_;
+ }
+
+ png_byte channels() const {
+ return channels_;
+ }
+
+ int bit_depth() const {
+ return bit_depth_;
+ }
+
+ int color_type() const {
+ return color_type_;
+ }
+
+ png_structp png_ptr() const {
+ return png_ptr_;
+ }
+
+ png_infop info_ptr() const {
+ return info_ptr_;
+ }
+
+ int error_code() const {
+ return error_code_;
+ };
+
+ operator bool() const {
+ return error_code_ == 0;
+ }
+
+ private:
+ png_structp png_ptr_{ nullptr };
+ png_infop info_ptr_{ nullptr };
+ png_uint_32 width_;
+ png_uint_32 height_;
+ png_byte channels_;
+ int bit_depth_;
+ int color_type_;
+
+ // The |error_code_| is set to a negative value if an error occurs when opening the png file.
+ int error_code_{ 0 };
+ // After initialization, we'll keep the file pointer open before destruction of PngHandler.
+ std::unique_ptr<FILE, decltype(&fclose)> png_fp_{ nullptr, fclose };
+};
+
+// Overrides the default resource dir, for testing purpose.
+void res_set_resource_dir(const std::string&);
diff --git a/minui/mkfont.c b/minui/mkfont.c
deleted file mode 100644
index 61a5ede..0000000
--- a/minui/mkfont.c
+++ /dev/null
@@ -1,54 +0,0 @@
-#include <stdio.h>
-#include <stdlib.h>
-
-int main(int argc, char *argv)
-{
- unsigned n;
- unsigned char *x;
- unsigned m;
- unsigned run_val;
- unsigned run_count;
-
- n = gimp_image.width * gimp_image.height;
- m = 0;
- x = gimp_image.pixel_data;
-
- printf("struct {\n");
- printf(" unsigned width;\n");
- printf(" unsigned height;\n");
- printf(" unsigned cwidth;\n");
- printf(" unsigned cheight;\n");
- printf(" unsigned char rundata[];\n");
- printf("} font = {\n");
- printf(" .width = %d,\n .height = %d,\n .cwidth = %d,\n .cheight = %d,\n", gimp_image.width, gimp_image.height,
- gimp_image.width / 96, gimp_image.height);
- printf(" .rundata = {\n");
-
- run_val = (*x ? 0 : 255);
- run_count = 1;
- n--;
- x+=3;
-
- while(n-- > 0) {
- unsigned val = (*x ? 0 : 255);
- x+=3;
- if((val == run_val) && (run_count < 127)) {
- run_count++;
- } else {
-eject:
- printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
- run_val = val;
- run_count = 1;
- m += 5;
- if(m >= 75) {
- printf("\n");
- m = 0;
- }
- }
- }
- printf("0x%02x,",run_count | (run_val ? 0x80 : 0x00));
- printf("\n0x00,");
- printf("\n");
- printf(" }\n};\n");
- return 0;
-}
diff --git a/minui/resources.cpp b/minui/resources.cpp
index 52ab60b..477fbe2 100644
--- a/minui/resources.cpp
+++ b/minui/resources.cpp
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+#include "private/resources.h"
+
#include <fcntl.h>
#include <linux/fb.h>
#include <linux/kd.h>
@@ -30,7 +32,6 @@
#include <string>
#include <vector>
-#include <android-base/stringprintf.h>
#include <android-base/strings.h>
#include <png.h>
@@ -38,6 +39,8 @@
#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));
@@ -48,58 +51,13 @@
return surface;
}
-// This class handles the png file parsing. It also holds the ownership of the png pointer and the
-// opened file pointer. Both will be destroyed/closed when this object goes out of scope.
-class PngHandler {
- public:
- PngHandler(const std::string& name);
-
- ~PngHandler();
-
- png_uint_32 width() const {
- return width_;
- }
-
- png_uint_32 height() const {
- return height_;
- }
-
- png_byte channels() const {
- return channels_;
- }
-
- png_structp png_ptr() const {
- return png_ptr_;
- }
-
- png_infop info_ptr() const {
- return info_ptr_;
- }
-
- int error_code() const {
- return error_code_;
- };
-
- operator bool() const {
- return error_code_ == 0;
- }
-
- private:
- png_structp png_ptr_{ nullptr };
- png_infop info_ptr_{ nullptr };
- png_uint_32 width_;
- png_uint_32 height_;
- png_byte channels_;
-
- // The |error_code_| is set to a negative value if an error occurs when opening the png file.
- int error_code_;
- // After initialization, we'll keep the file pointer open before destruction of PngHandler.
- std::unique_ptr<FILE, decltype(&fclose)> png_fp_;
-};
-
-PngHandler::PngHandler(const std::string& name) : error_code_(0), png_fp_(nullptr, fclose) {
- std::string res_path = android::base::StringPrintf("/res/images/%s.png", name.c_str());
+PngHandler::PngHandler(const std::string& name) {
+ std::string res_path = g_resource_dir + "/" + name + ".png";
png_fp_.reset(fopen(res_path.c_str(), "rbe"));
+ // Try to read from |name| if the resource path does not work.
+ if (!png_fp_) {
+ png_fp_.reset(fopen(name.c_str(), "rbe"));
+ }
if (!png_fp_) {
error_code_ = -1;
return;
@@ -138,19 +96,17 @@
png_set_sig_bytes(png_ptr_, sizeof(header));
png_read_info(png_ptr_, info_ptr_);
- int color_type;
- int bit_depth;
- png_get_IHDR(png_ptr_, info_ptr_, &width_, &height_, &bit_depth, &color_type, nullptr, nullptr,
+ png_get_IHDR(png_ptr_, info_ptr_, &width_, &height_, &bit_depth_, &color_type_, nullptr, nullptr,
nullptr);
channels_ = png_get_channels(png_ptr_, info_ptr_);
- if (bit_depth == 8 && channels_ == 3 && color_type == PNG_COLOR_TYPE_RGB) {
+ if (bit_depth_ == 8 && channels_ == 3 && color_type_ == PNG_COLOR_TYPE_RGB) {
// 8-bit RGB images: great, nothing to do.
- } else if (bit_depth <= 8 && channels_ == 1 && color_type == PNG_COLOR_TYPE_GRAY) {
+ } else if (bit_depth_ <= 8 && channels_ == 1 && color_type_ == PNG_COLOR_TYPE_GRAY) {
// 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
png_set_expand_gray_1_2_4_to_8(png_ptr_);
- } else if (bit_depth <= 8 && channels_ == 1 && color_type == PNG_COLOR_TYPE_PALETTE) {
+ } else if (bit_depth_ <= 8 && channels_ == 1 && color_type_ == PNG_COLOR_TYPE_PALETTE) {
// paletted images: expand to 8-bit RGB. Note that we DON'T
// currently expand the tRNS chunk (if any) to an alpha
// channel, because minui doesn't support alpha channels in
@@ -158,8 +114,8 @@
png_set_palette_to_rgb(png_ptr_);
channels_ = 3;
} else {
- fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n", bit_depth,
- channels_, color_type);
+ fprintf(stderr, "minui doesn't support PNG depth %d channels %d color_type %d\n", bit_depth_,
+ channels_, color_type_);
error_code_ = -7;
}
}
@@ -251,9 +207,10 @@
return -8;
}
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
- png_set_bgr(png_ptr);
-#endif
+ PixelFormat pixel_format = gr_pixel_format();
+ if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+ png_set_bgr(png_ptr);
+ }
for (png_uint_32 y = 0; y < height; ++y) {
std::vector<unsigned char> p_row(width * 4);
@@ -322,9 +279,9 @@
}
}
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
- png_set_bgr(png_ptr);
-#endif
+ if (gr_pixel_format() == PixelFormat::ABGR || gr_pixel_format() == PixelFormat::BGRA) {
+ png_set_bgr(png_ptr);
+ }
for (png_uint_32 y = 0; y < height; ++y) {
std::vector<unsigned char> p_row(width * 4);
@@ -371,9 +328,10 @@
surface->row_bytes = width;
surface->pixel_bytes = 1;
-#if defined(RECOVERY_ABGR) || defined(RECOVERY_BGRA)
- png_set_bgr(png_ptr);
-#endif
+ PixelFormat pixel_format = gr_pixel_format();
+ if (pixel_format == PixelFormat::ABGR || pixel_format == PixelFormat::BGRA) {
+ png_set_bgr(png_ptr);
+ }
for (png_uint_32 y = 0; y < height; ++y) {
unsigned char* p_row = surface->data + y * surface->row_bytes;
@@ -385,6 +343,10 @@
return 0;
}
+void res_set_resource_dir(const std::string& dirname) {
+ g_resource_dir = dirname;
+}
+
// This function tests if a locale string stored in PNG (prefix) matches
// the locale string provided by the system (locale).
bool matches_locale(const std::string& prefix, const std::string& locale) {
diff --git a/otafault/Android.bp b/otafault/Android.bp
deleted file mode 100644
index b39d5be..0000000
--- a/otafault/Android.bp
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2017 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-cc_library_static {
- name: "libotafault",
-
- host_supported: true,
-
- srcs: [
- "config.cpp",
- "ota_io.cpp",
- ],
-
- static_libs: [
- "libbase",
- "liblog",
- "libziparchive",
- ],
-
- export_include_dirs: [
- "include",
- ],
-
- cflags: [
- "-D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS",
- "-Wall",
- "-Werror",
- "-Wthread-safety",
- "-Wthread-safety-negative",
- ],
-
- target: {
- darwin: {
- enabled: false,
- },
- },
-}
-
-cc_test {
- name: "otafault_test",
-
- srcs: ["test.cpp"],
-
- cflags: [
- "-Wall",
- "-Werror",
- ],
-
- static_executable: true,
-
- static_libs: [
- "libotafault",
- "libziparchive",
- "libbase",
- "liblog",
- ],
-}
diff --git a/otafault/config.cpp b/otafault/config.cpp
deleted file mode 100644
index 3993948..0000000
--- a/otafault/config.cpp
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "otafault/config.h"
-
-#include <map>
-#include <string>
-
-#include <android-base/stringprintf.h>
-#include <ziparchive/zip_archive.h>
-
-#include "otafault/ota_io.h"
-
-#define OTAIO_MAX_FNAME_SIZE 128
-
-static ZipArchiveHandle archive;
-static bool is_retry = false;
-static std::map<std::string, bool> should_inject_cache;
-
-static std::string get_type_path(const char* io_type) {
- return android::base::StringPrintf("%s/%s", OTAIO_BASE_DIR, io_type);
-}
-
-void ota_io_init(ZipArchiveHandle za, bool retry) {
- archive = za;
- is_retry = retry;
- ota_set_fault_files();
-}
-
-bool should_fault_inject(const char* io_type) {
- // archive will be NULL if we used an entry point other
- // than updater/updater.cpp:main
- if (archive == nullptr || is_retry) {
- return false;
- }
- const std::string type_path = get_type_path(io_type);
- if (should_inject_cache.find(type_path) != should_inject_cache.end()) {
- return should_inject_cache[type_path];
- }
- ZipString zip_type_path(type_path.c_str());
- ZipEntry entry;
- int status = FindEntry(archive, zip_type_path, &entry);
- should_inject_cache[type_path] = (status == 0);
- return (status == 0);
-}
-
-bool should_hit_cache() {
- return should_fault_inject(OTAIO_CACHE);
-}
-
-std::string fault_fname(const char* io_type) {
- std::string type_path = get_type_path(io_type);
- std::string fname;
- fname.resize(OTAIO_MAX_FNAME_SIZE);
- ZipString zip_type_path(type_path.c_str());
- ZipEntry entry;
- if (FindEntry(archive, zip_type_path, &entry) != 0) {
- return {};
- }
- ExtractToMemory(archive, &entry, reinterpret_cast<uint8_t*>(&fname[0]), OTAIO_MAX_FNAME_SIZE);
- return fname;
-}
diff --git a/otafault/include/otafault/config.h b/otafault/include/otafault/config.h
deleted file mode 100644
index cc4bfd2..0000000
--- a/otafault/include/otafault/config.h
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Read configuration files in the OTA package to determine which files, if any, will trigger
- * errors.
- *
- * OTA packages can be modified to trigger errors by adding a top-level directory called
- * .libotafault, which may optionally contain up to three files called READ, WRITE, and FSYNC.
- * Each one of these optional files contains the name of a single file on the device disk which
- * will cause an IO error on the first call of the appropriate I/O action to that file.
- *
- * Example:
- * ota.zip
- * <normal package contents>
- * .libotafault
- * WRITE
- *
- * If the contents of the file WRITE were /system/build.prop, the first write action to
- * /system/build.prop would fail with EIO. Note that READ and FSYNC files are absent, so these
- * actions will not cause an error.
- */
-
-#ifndef _UPDATER_OTA_IO_CFG_H_
-#define _UPDATER_OTA_IO_CFG_H_
-
-#include <string>
-
-#include <ziparchive/zip_archive.h>
-
-#define OTAIO_BASE_DIR ".libotafault"
-#define OTAIO_READ "READ"
-#define OTAIO_WRITE "WRITE"
-#define OTAIO_FSYNC "FSYNC"
-#define OTAIO_CACHE "CACHE"
-
-/*
- * Initialize libotafault by providing a reference to the OTA package.
- */
-void ota_io_init(ZipArchiveHandle zip, bool retry);
-
-/*
- * Return true if a config file is present for the given IO type.
- */
-bool should_fault_inject(const char* io_type);
-
-/*
- * Return true if an EIO should occur on the next hit to /cache/saved.file
- * instead of the next hit to the specified file.
- */
-bool should_hit_cache();
-
-/*
- * Return the name of the file that should cause an error for the
- * given IO type.
- */
-std::string fault_fname(const char* io_type);
-
-#endif
diff --git a/otafault/include/otafault/ota_io.h b/otafault/include/otafault/ota_io.h
deleted file mode 100644
index 45e481a..0000000
--- a/otafault/include/otafault/ota_io.h
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-/*
- * Provide a series of proxy functions for basic file accessors.
- * The behavior of these functions can be changed to return different
- * errors under a variety of conditions.
- */
-
-#ifndef _UPDATER_OTA_IO_H_
-#define _UPDATER_OTA_IO_H_
-
-#include <stddef.h>
-#include <stdio.h>
-#include <sys/stat.h> // mode_t
-
-#include <memory>
-
-#include <android-base/unique_fd.h>
-
-#define OTAIO_CACHE_FNAME "/cache/saved.file"
-
-void ota_set_fault_files();
-
-int ota_open(const char* path, int oflags);
-
-int ota_open(const char* path, int oflags, mode_t mode);
-
-FILE* ota_fopen(const char* filename, const char* mode);
-
-size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream);
-
-ssize_t ota_read(int fd, void* buf, size_t nbyte);
-
-size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream);
-
-ssize_t ota_write(int fd, const void* buf, size_t nbyte);
-
-int ota_fsync(int fd);
-
-struct OtaCloser {
- static void Close(int);
-};
-
-using unique_fd = android::base::unique_fd_impl<OtaCloser>;
-
-int ota_close(unique_fd& fd);
-
-struct OtaFcloser {
- void operator()(FILE*) const;
-};
-
-using unique_file = std::unique_ptr<FILE, OtaFcloser>;
-
-int ota_fclose(unique_file& fh);
-
-#endif
diff --git a/otafault/ota_io.cpp b/otafault/ota_io.cpp
deleted file mode 100644
index 63ef18e..0000000
--- a/otafault/ota_io.cpp
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "otafault/ota_io.h"
-
-#include <errno.h>
-#include <fcntl.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include <map>
-#include <mutex>
-#include <string>
-
-#include <android-base/thread_annotations.h>
-
-#include "otafault/config.h"
-
-static std::mutex filename_mutex;
-static std::map<intptr_t, const char*> filename_cache GUARDED_BY(filename_mutex);
-static std::string read_fault_file_name = "";
-static std::string write_fault_file_name = "";
-static std::string fsync_fault_file_name = "";
-
-static bool get_hit_file(const char* cached_path, const std::string& ffn) {
- return should_hit_cache()
- ? !strncmp(cached_path, OTAIO_CACHE_FNAME, strlen(cached_path))
- : !strncmp(cached_path, ffn.c_str(), strlen(cached_path));
-}
-
-void ota_set_fault_files() {
- if (should_fault_inject(OTAIO_READ)) {
- read_fault_file_name = fault_fname(OTAIO_READ);
- }
- if (should_fault_inject(OTAIO_WRITE)) {
- write_fault_file_name = fault_fname(OTAIO_WRITE);
- }
- if (should_fault_inject(OTAIO_FSYNC)) {
- fsync_fault_file_name = fault_fname(OTAIO_FSYNC);
- }
-}
-
-bool have_eio_error = false;
-
-int ota_open(const char* path, int oflags) {
- // Let the caller handle errors; we do not care if open succeeds or fails
- int fd = open(path, oflags);
- std::lock_guard<std::mutex> lock(filename_mutex);
- filename_cache[fd] = path;
- return fd;
-}
-
-int ota_open(const char* path, int oflags, mode_t mode) {
- int fd = open(path, oflags, mode);
- std::lock_guard<std::mutex> lock(filename_mutex);
- filename_cache[fd] = path;
- return fd;
-}
-
-FILE* ota_fopen(const char* path, const char* mode) {
- FILE* fh = fopen(path, mode);
- std::lock_guard<std::mutex> lock(filename_mutex);
- filename_cache[(intptr_t)fh] = path;
- return fh;
-}
-
-static int __ota_close(int fd) {
- // descriptors can be reused, so make sure not to leave them in the cache
- std::lock_guard<std::mutex> lock(filename_mutex);
- filename_cache.erase(fd);
- return close(fd);
-}
-
-void OtaCloser::Close(int fd) {
- __ota_close(fd);
-}
-
-int ota_close(unique_fd& fd) {
- return __ota_close(fd.release());
-}
-
-static int __ota_fclose(FILE* fh) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- filename_cache.erase(reinterpret_cast<intptr_t>(fh));
- return fclose(fh);
-}
-
-void OtaFcloser::operator()(FILE* f) const {
- __ota_fclose(f);
-};
-
-int ota_fclose(unique_file& fh) {
- return __ota_fclose(fh.release());
-}
-
-size_t ota_fread(void* ptr, size_t size, size_t nitems, FILE* stream) {
- if (should_fault_inject(OTAIO_READ)) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- auto cached = filename_cache.find((intptr_t)stream);
- const char* cached_path = cached->second;
- if (cached != filename_cache.end() &&
- get_hit_file(cached_path, read_fault_file_name)) {
- read_fault_file_name = "";
- errno = EIO;
- have_eio_error = true;
- return 0;
- }
- }
- size_t status = fread(ptr, size, nitems, stream);
- // If I/O error occurs, set the retry-update flag.
- if (status != nitems && errno == EIO) {
- have_eio_error = true;
- }
- return status;
-}
-
-ssize_t ota_read(int fd, void* buf, size_t nbyte) {
- if (should_fault_inject(OTAIO_READ)) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- auto cached = filename_cache.find(fd);
- const char* cached_path = cached->second;
- if (cached != filename_cache.end()
- && get_hit_file(cached_path, read_fault_file_name)) {
- read_fault_file_name = "";
- errno = EIO;
- have_eio_error = true;
- return -1;
- }
- }
- ssize_t status = read(fd, buf, nbyte);
- if (status == -1 && errno == EIO) {
- have_eio_error = true;
- }
- return status;
-}
-
-size_t ota_fwrite(const void* ptr, size_t size, size_t count, FILE* stream) {
- if (should_fault_inject(OTAIO_WRITE)) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- auto cached = filename_cache.find((intptr_t)stream);
- const char* cached_path = cached->second;
- if (cached != filename_cache.end() &&
- get_hit_file(cached_path, write_fault_file_name)) {
- write_fault_file_name = "";
- errno = EIO;
- have_eio_error = true;
- return 0;
- }
- }
- size_t status = fwrite(ptr, size, count, stream);
- if (status != count && errno == EIO) {
- have_eio_error = true;
- }
- return status;
-}
-
-ssize_t ota_write(int fd, const void* buf, size_t nbyte) {
- if (should_fault_inject(OTAIO_WRITE)) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- auto cached = filename_cache.find(fd);
- const char* cached_path = cached->second;
- if (cached != filename_cache.end() &&
- get_hit_file(cached_path, write_fault_file_name)) {
- write_fault_file_name = "";
- errno = EIO;
- have_eio_error = true;
- return -1;
- }
- }
- ssize_t status = write(fd, buf, nbyte);
- if (status == -1 && errno == EIO) {
- have_eio_error = true;
- }
- return status;
-}
-
-int ota_fsync(int fd) {
- if (should_fault_inject(OTAIO_FSYNC)) {
- std::lock_guard<std::mutex> lock(filename_mutex);
- auto cached = filename_cache.find(fd);
- const char* cached_path = cached->second;
- if (cached != filename_cache.end() &&
- get_hit_file(cached_path, fsync_fault_file_name)) {
- fsync_fault_file_name = "";
- errno = EIO;
- have_eio_error = true;
- return -1;
- }
- }
- int status = fsync(fd);
- if (status == -1 && errno == EIO) {
- have_eio_error = true;
- }
- return status;
-}
-
diff --git a/otafault/test.cpp b/otafault/test.cpp
deleted file mode 100644
index 63e2445..0000000
--- a/otafault/test.cpp
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <fcntl.h>
-#include <stdio.h>
-#include <sys/stat.h>
-#include <sys/types.h>
-#include <unistd.h>
-
-#include "otafault/ota_io.h"
-
-int main(int /* argc */, char** /* argv */) {
- int fd = open("testdata/test.file", O_RDWR);
- char buf[8];
- const char* out = "321";
- int readv = ota_read(fd, buf, 4);
- printf("Read returned %d\n", readv);
- int writev = ota_write(fd, out, 4);
- printf("Write returned %d\n", writev);
- close(fd);
- return 0;
-}
diff --git a/otautil/Android.bp b/otautil/Android.bp
index 75cf691..41018dd 100644
--- a/otautil/Android.bp
+++ b/otautil/Android.bp
@@ -16,27 +16,40 @@
name: "libotautil",
host_supported: true,
+ recovery_available: true,
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ // Minimal set of files to support host build.
srcs: [
- "SysUtil.cpp",
- "DirUtil.cpp",
- "ThermalUtil.cpp",
- "cache_location.cpp",
+ "paths.cpp",
"rangeset.cpp",
],
- static_libs: [
- "libselinux",
+ shared_libs: [
"libbase",
],
- cflags: [
- "-D_FILE_OFFSET_BITS=64",
- "-Werror",
- "-Wall",
- ],
-
export_include_dirs: [
"include",
],
+
+ target: {
+ android: {
+ srcs: [
+ "dirutil.cpp",
+ "mounts.cpp",
+ "parse_install_logs.cpp",
+ "sysutil.cpp",
+ "thermalutil.cpp",
+ ],
+
+ shared_libs: [
+ "libcutils",
+ "libselinux",
+ ],
+ },
+ },
}
diff --git a/otautil/cache_location.cpp b/otautil/cache_location.cpp
deleted file mode 100644
index 8ddefec..0000000
--- a/otautil/cache_location.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "otautil/cache_location.h"
-
-constexpr const char kDefaultCacheTempSource[] = "/cache/saved.file";
-constexpr const char kDefaultLastCommandFile[] = "/cache/recovery/last_command";
-constexpr const char kDefaultStashDirectoryBase[] = "/cache/recovery";
-
-CacheLocation& CacheLocation::location() {
- static CacheLocation cache_location;
- return cache_location;
-}
-
-CacheLocation::CacheLocation()
- : cache_temp_source_(kDefaultCacheTempSource),
- last_command_file_(kDefaultLastCommandFile),
- stash_directory_base_(kDefaultStashDirectoryBase) {}
diff --git a/otautil/DirUtil.cpp b/otautil/dirutil.cpp
similarity index 98%
rename from otautil/DirUtil.cpp
rename to otautil/dirutil.cpp
index 61c8328..ae1cd5c 100644
--- a/otautil/DirUtil.cpp
+++ b/otautil/dirutil.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "otautil/DirUtil.h"
+#include "otautil/dirutil.h"
#include <dirent.h>
#include <errno.h>
diff --git a/otautil/include/otautil/cache_location.h b/otautil/include/otautil/cache_location.h
deleted file mode 100644
index f2f6638..0000000
--- a/otautil/include/otautil/cache_location.h
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Copyright (C) 2018 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
-#define _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
-
-#include <string>
-
-#include "android-base/macros.h"
-
-// A singleton class to maintain the update related locations. The locations should be only set
-// once at the start of the program.
-class CacheLocation {
- public:
- static CacheLocation& location();
-
- // getter and setter functions.
- std::string cache_temp_source() const {
- return cache_temp_source_;
- }
- void set_cache_temp_source(const std::string& temp_source) {
- cache_temp_source_ = temp_source;
- }
-
- std::string last_command_file() const {
- return last_command_file_;
- }
- void set_last_command_file(const std::string& last_command) {
- last_command_file_ = last_command;
- }
-
- std::string stash_directory_base() const {
- return stash_directory_base_;
- }
- void set_stash_directory_base(const std::string& base) {
- stash_directory_base_ = base;
- }
-
- private:
- CacheLocation();
- DISALLOW_COPY_AND_ASSIGN(CacheLocation);
-
- // When there isn't enough room on the target filesystem to hold the patched version of the file,
- // we copy the original here and delete it to free up space. If the expected source file doesn't
- // exist, or is corrupted, we look to see if the cached file contains the bits we want and use it
- // as the source instead. The default location for the cached source is "/cache/saved.file".
- std::string cache_temp_source_;
-
- // Location to save the last command that stashes blocks.
- std::string last_command_file_;
-
- // The base directory to write stashes during update.
- std::string stash_directory_base_;
-};
-
-#endif // _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
diff --git a/otautil/include/otautil/DirUtil.h b/otautil/include/otautil/dirutil.h
similarity index 100%
rename from otautil/include/otautil/DirUtil.h
rename to otautil/include/otautil/dirutil.h
diff --git a/otautil/include/otautil/error_code.h b/otautil/include/otautil/error_code.h
index b0ff42d..2b73c13 100644
--- a/otautil/include/otautil/error_code.h
+++ b/otautil/include/otautil/error_code.h
@@ -48,6 +48,8 @@
kRebootFailure,
kPackageExtractFileFailure,
kPatchApplicationFailure,
+ kHashTreeComputationFailure,
+ kEioFailure,
kVendorFailure = 200
};
diff --git a/mounts.h b/otautil/include/otautil/mounts.h
similarity index 94%
rename from mounts.h
rename to otautil/include/otautil/mounts.h
index 0de1ebd..6786c8d 100644
--- a/mounts.h
+++ b/otautil/include/otautil/mounts.h
@@ -14,8 +14,7 @@
* limitations under the License.
*/
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
struct MountedVolume;
@@ -24,5 +23,3 @@
MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
diff --git a/otautil/include/otautil/parse_install_logs.h b/otautil/include/otautil/parse_install_logs.h
new file mode 100644
index 0000000..135d29c
--- /dev/null
+++ b/otautil/include/otautil/parse_install_logs.h
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <map>
+#include <string>
+#include <vector>
+
+constexpr const char* LAST_INSTALL_FILE = "/data/misc/recovery/last_install";
+constexpr const char* LAST_INSTALL_FILE_IN_CACHE = "/cache/recovery/last_install";
+
+// Parses the metrics of update applied under recovery mode in |lines|, and returns a map with
+// "name: value".
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines);
+// Parses the sideload history and update metrics in the last_install file. Returns a map with
+// entries as "metrics_name: value". If no such file exists, returns an empty map.
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name);
diff --git a/otautil/include/otautil/paths.h b/otautil/include/otautil/paths.h
new file mode 100644
index 0000000..f95741a
--- /dev/null
+++ b/otautil/include/otautil/paths.h
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef _OTAUTIL_PATHS_H_
+#define _OTAUTIL_PATHS_H_
+
+#include <string>
+
+#include <android-base/macros.h>
+
+// A singleton class to maintain the update related paths. The paths should be only set once at the
+// start of the program.
+class Paths {
+ public:
+ static Paths& Get();
+
+ std::string cache_log_directory() const {
+ return cache_log_directory_;
+ }
+ void set_cache_log_directory(const std::string& log_dir) {
+ cache_log_directory_ = log_dir;
+ }
+
+ std::string cache_temp_source() const {
+ return cache_temp_source_;
+ }
+ void set_cache_temp_source(const std::string& temp_source) {
+ cache_temp_source_ = temp_source;
+ }
+
+ std::string last_command_file() const {
+ return last_command_file_;
+ }
+ void set_last_command_file(const std::string& last_command_file) {
+ last_command_file_ = last_command_file;
+ }
+
+ std::string resource_dir() const {
+ return resource_dir_;
+ }
+ void set_resource_dir(const std::string& resource_dir) {
+ resource_dir_ = resource_dir;
+ }
+
+ std::string stash_directory_base() const {
+ return stash_directory_base_;
+ }
+ void set_stash_directory_base(const std::string& base) {
+ stash_directory_base_ = base;
+ }
+
+ std::string temporary_install_file() const {
+ return temporary_install_file_;
+ }
+ void set_temporary_install_file(const std::string& install_file) {
+ temporary_install_file_ = install_file;
+ }
+
+ std::string temporary_log_file() const {
+ return temporary_log_file_;
+ }
+ void set_temporary_log_file(const std::string& log_file) {
+ temporary_log_file_ = log_file;
+ }
+
+ std::string temporary_update_binary() const {
+ return temporary_update_binary_;
+ }
+ void set_temporary_update_binary(const std::string& update_binary) {
+ temporary_update_binary_ = update_binary;
+ }
+
+ private:
+ Paths();
+ DISALLOW_COPY_AND_ASSIGN(Paths);
+
+ // Path to the directory that contains last_log and last_kmsg log files.
+ std::string cache_log_directory_;
+
+ // Path to the temporary source file on /cache. When there isn't enough room on the target
+ // filesystem to hold the patched version of the file, we copy the original here and delete it to
+ // free up space. If the expected source file doesn't exist, or is corrupted, we look to see if
+ // the cached file contains the bits we want and use it as the source instead.
+ std::string cache_temp_source_;
+
+ // Path to the last command file.
+ std::string last_command_file_;
+
+ // Path to the resource dir;
+ std::string resource_dir_;
+
+ // Path to the base directory to write stashes during update.
+ std::string stash_directory_base_;
+
+ // Path to the temporary file that contains the install result.
+ std::string temporary_install_file_;
+
+ // Path to the temporary log file while under recovery.
+ std::string temporary_log_file_;
+
+ // Path to the temporary update binary while installing a non-A/B package.
+ std::string temporary_update_binary_;
+};
+
+#endif // _OTAUTIL_PATHS_H_
diff --git a/otautil/include/otautil/SysUtil.h b/otautil/include/otautil/sysutil.h
similarity index 71%
rename from otautil/include/otautil/SysUtil.h
rename to otautil/include/otautil/sysutil.h
index 52f6d20..2eeb7c3 100644
--- a/otautil/include/otautil/SysUtil.h
+++ b/otautil/include/otautil/sysutil.h
@@ -50,4 +50,13 @@
std::vector<MappedRange> ranges_;
};
+// Wrapper function to trigger a reboot, by additionally handling quiescent reboot mode. The
+// command should start with "reboot," (e.g. "reboot,bootloader" or "reboot,").
+bool reboot(const std::string& command);
+
+// Returns a null-terminated char* array, where the elements point to the C-strings in the given
+// vector, plus an additional nullptr at the end. This is a helper function that facilitates
+// calling C functions (such as getopt(3)) that expect an array of C-strings.
+std::vector<char*> StringVectorToNullTerminatedArray(const std::vector<std::string>& args);
+
#endif // _OTAUTIL_SYSUTIL
diff --git a/otautil/include/otautil/ThermalUtil.h b/otautil/include/otautil/thermalutil.h
similarity index 100%
rename from otautil/include/otautil/ThermalUtil.h
rename to otautil/include/otautil/thermalutil.h
diff --git a/mounts.cpp b/otautil/mounts.cpp
similarity index 60%
rename from mounts.cpp
rename to otautil/mounts.cpp
index 76fa657..951311b 100644
--- a/mounts.cpp
+++ b/otautil/mounts.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "mounts.h"
+#include "otautil/mounts.h"
#include <errno.h>
#include <fcntl.h>
@@ -30,43 +30,43 @@
#include <android-base/logging.h>
struct MountedVolume {
- std::string device;
- std::string mount_point;
- std::string filesystem;
- std::string flags;
+ std::string device;
+ std::string mount_point;
+ std::string filesystem;
+ std::string flags;
};
-std::vector<MountedVolume*> g_mounts_state;
+static std::vector<MountedVolume*> g_mounts_state;
bool scan_mounted_volumes() {
- for (size_t i = 0; i < g_mounts_state.size(); ++i) {
- delete g_mounts_state[i];
- }
- g_mounts_state.clear();
+ for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+ delete g_mounts_state[i];
+ }
+ g_mounts_state.clear();
- // Open and read mount table entries.
- FILE* fp = setmntent("/proc/mounts", "re");
- if (fp == NULL) {
- return false;
- }
- mntent* e;
- while ((e = getmntent(fp)) != NULL) {
- MountedVolume* v = new MountedVolume;
- v->device = e->mnt_fsname;
- v->mount_point = e->mnt_dir;
- v->filesystem = e->mnt_type;
- v->flags = e->mnt_opts;
- g_mounts_state.push_back(v);
- }
- endmntent(fp);
- return true;
+ // Open and read mount table entries.
+ FILE* fp = setmntent("/proc/mounts", "re");
+ if (fp == NULL) {
+ return false;
+ }
+ mntent* e;
+ while ((e = getmntent(fp)) != NULL) {
+ MountedVolume* v = new MountedVolume;
+ v->device = e->mnt_fsname;
+ v->mount_point = e->mnt_dir;
+ v->filesystem = e->mnt_type;
+ v->flags = e->mnt_opts;
+ g_mounts_state.push_back(v);
+ }
+ endmntent(fp);
+ return true;
}
MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point) {
- for (size_t i = 0; i < g_mounts_state.size(); ++i) {
- if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i];
- }
- return nullptr;
+ for (size_t i = 0; i < g_mounts_state.size(); ++i) {
+ if (g_mounts_state[i]->mount_point == mount_point) return g_mounts_state[i];
+ }
+ return nullptr;
}
int unmount_mounted_volume(MountedVolume* volume) {
diff --git a/otautil/parse_install_logs.cpp b/otautil/parse_install_logs.cpp
new file mode 100644
index 0000000..13a7299
--- /dev/null
+++ b/otautil/parse_install_logs.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "otautil/parse_install_logs.h"
+
+#include <unistd.h>
+
+#include <optional>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+
+constexpr const char* OTA_SIDELOAD_METRICS = "ota_sideload";
+
+// Here is an example of lines in last_install:
+// ...
+// time_total: 101
+// bytes_written_vendor: 51074
+// bytes_stashed_vendor: 200
+std::map<std::string, int64_t> ParseRecoveryUpdateMetrics(const std::vector<std::string>& lines) {
+ constexpr unsigned int kMiB = 1024 * 1024;
+ std::optional<int64_t> bytes_written_in_mib;
+ std::optional<int64_t> bytes_stashed_in_mib;
+ std::map<std::string, int64_t> metrics;
+ for (const auto& line : lines) {
+ size_t num_index = line.find(':');
+ if (num_index == std::string::npos) {
+ LOG(WARNING) << "Skip parsing " << line;
+ continue;
+ }
+
+ std::string num_string = android::base::Trim(line.substr(num_index + 1));
+ int64_t parsed_num;
+ if (!android::base::ParseInt(num_string, &parsed_num)) {
+ LOG(ERROR) << "Failed to parse numbers in " << line;
+ continue;
+ }
+
+ if (android::base::StartsWith(line, "bytes_written")) {
+ bytes_written_in_mib = bytes_written_in_mib.value_or(0) + parsed_num / kMiB;
+ } else if (android::base::StartsWith(line, "bytes_stashed")) {
+ bytes_stashed_in_mib = bytes_stashed_in_mib.value_or(0) + parsed_num / kMiB;
+ } else if (android::base::StartsWith(line, "time")) {
+ metrics.emplace("ota_time_total", parsed_num);
+ } else if (android::base::StartsWith(line, "uncrypt_time")) {
+ metrics.emplace("ota_uncrypt_time", parsed_num);
+ } else if (android::base::StartsWith(line, "source_build")) {
+ metrics.emplace("ota_source_version", parsed_num);
+ } else if (android::base::StartsWith(line, "temperature_start")) {
+ metrics.emplace("ota_temperature_start", parsed_num);
+ } else if (android::base::StartsWith(line, "temperature_end")) {
+ metrics.emplace("ota_temperature_end", parsed_num);
+ } else if (android::base::StartsWith(line, "temperature_max")) {
+ metrics.emplace("ota_temperature_max", parsed_num);
+ } else if (android::base::StartsWith(line, "error")) {
+ metrics.emplace("ota_non_ab_error_code", parsed_num);
+ } else if (android::base::StartsWith(line, "cause")) {
+ metrics.emplace("ota_non_ab_cause_code", parsed_num);
+ }
+ }
+
+ if (bytes_written_in_mib) {
+ metrics.emplace("ota_written_in_MiBs", bytes_written_in_mib.value());
+ }
+ if (bytes_stashed_in_mib) {
+ metrics.emplace("ota_stashed_in_MiBs", bytes_stashed_in_mib.value());
+ }
+
+ return metrics;
+}
+
+std::map<std::string, int64_t> ParseLastInstall(const std::string& file_name) {
+ if (access(file_name.c_str(), F_OK) != 0) {
+ return {};
+ }
+
+ std::string content;
+ if (!android::base::ReadFileToString(file_name, &content)) {
+ PLOG(ERROR) << "Failed to read " << file_name;
+ return {};
+ }
+
+ if (content.empty()) {
+ LOG(INFO) << "Empty last_install file";
+ return {};
+ }
+
+ std::vector<std::string> lines = android::base::Split(content, "\n");
+ auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+ // LAST_INSTALL starts with "/sideload/package.zip" after a sideload.
+ if (android::base::Trim(lines[0]) == "/sideload/package.zip") {
+ int type = (android::base::GetProperty("ro.build.type", "") == "user") ? 1 : 0;
+ metrics.emplace(OTA_SIDELOAD_METRICS, type);
+ }
+
+ return metrics;
+}
diff --git a/otautil/paths.cpp b/otautil/paths.cpp
new file mode 100644
index 0000000..33ab4a5
--- /dev/null
+++ b/otautil/paths.cpp
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "otautil/paths.h"
+
+constexpr const char kDefaultCacheLogDirectory[] = "/cache/recovery";
+constexpr const char kDefaultCacheTempSource[] = "/cache/saved.file";
+constexpr const char kDefaultLastCommandFile[] = "/cache/recovery/last_command";
+constexpr const char kDefaultResourceDirectory[] = "/res/images";
+constexpr const char kDefaultStashDirectoryBase[] = "/cache/recovery";
+constexpr const char kDefaultTemporaryInstallFile[] = "/tmp/last_install";
+constexpr const char kDefaultTemporaryLogFile[] = "/tmp/recovery.log";
+constexpr const char kDefaultTemporaryUpdateBinary[] = "/tmp/update-binary";
+
+Paths& Paths::Get() {
+ static Paths paths;
+ return paths;
+}
+
+Paths::Paths()
+ : cache_log_directory_(kDefaultCacheLogDirectory),
+ cache_temp_source_(kDefaultCacheTempSource),
+ last_command_file_(kDefaultLastCommandFile),
+ resource_dir_(kDefaultResourceDirectory),
+ stash_directory_base_(kDefaultStashDirectoryBase),
+ temporary_install_file_(kDefaultTemporaryInstallFile),
+ temporary_log_file_(kDefaultTemporaryLogFile),
+ temporary_update_binary_(kDefaultTemporaryUpdateBinary) {}
diff --git a/otautil/rangeset.cpp b/otautil/rangeset.cpp
index 96955b9..5ab8e08 100644
--- a/otautil/rangeset.cpp
+++ b/otautil/rangeset.cpp
@@ -148,8 +148,8 @@
return "";
}
std::string result = std::to_string(ranges_.size() * 2);
- for (const auto& r : ranges_) {
- result += android::base::StringPrintf(",%zu,%zu", r.first, r.second);
+ for (const auto& [begin, end] : ranges_) {
+ result += android::base::StringPrintf(",%zu,%zu", begin, end);
}
return result;
@@ -159,11 +159,11 @@
size_t RangeSet::GetBlockNumber(size_t idx) const {
CHECK_LT(idx, blocks_) << "Out of bound index " << idx << " (total blocks: " << blocks_ << ")";
- for (const auto& range : ranges_) {
- if (idx < range.second - range.first) {
- return range.first + idx;
+ for (const auto& [begin, end] : ranges_) {
+ if (idx < end - begin) {
+ return begin + idx;
}
- idx -= (range.second - range.first);
+ idx -= (end - begin);
}
CHECK(false) << "Failed to find block number for index " << idx;
@@ -173,14 +173,10 @@
// RangeSet has half-closed half-open bounds. For example, "3,5" contains blocks 3 and 4. So "3,5"
// and "5,7" are not overlapped.
bool RangeSet::Overlaps(const RangeSet& other) const {
- for (const auto& range : ranges_) {
- size_t start = range.first;
- size_t end = range.second;
- for (const auto& other_range : other.ranges_) {
- size_t other_start = other_range.first;
- size_t other_end = other_range.second;
- // [start, end) vs [other_start, other_end)
- if (!(other_start >= end || start >= other_end)) {
+ for (const auto& [begin, end] : ranges_) {
+ for (const auto& [other_begin, other_end] : other.ranges_) {
+ // [begin, end) vs [other_begin, other_end)
+ if (!(other_begin >= end || begin >= other_end)) {
return true;
}
}
@@ -248,20 +244,20 @@
size_t SortedRangeSet::GetOffsetInRangeSet(size_t old_offset) const {
size_t old_block_start = old_offset / kBlockSize;
size_t new_block_start = 0;
- for (const auto& range : ranges_) {
+ for (const auto& [start, end] : ranges_) {
// Find the index of old_block_start.
- if (old_block_start >= range.second) {
- new_block_start += (range.second - range.first);
- } else if (old_block_start >= range.first) {
- new_block_start += (old_block_start - range.first);
+ if (old_block_start >= end) {
+ new_block_start += (end - start);
+ } else if (old_block_start >= start) {
+ new_block_start += (old_block_start - start);
return (new_block_start * kBlockSize + old_offset % kBlockSize);
} else {
CHECK(false) << "block_start " << old_block_start
- << " is missing between two ranges: " << this->ToString();
+ << " is missing between two ranges: " << ToString();
return 0;
}
}
CHECK(false) << "block_start " << old_block_start
- << " exceeds the limit of current RangeSet: " << this->ToString();
+ << " exceeds the limit of current RangeSet: " << ToString();
return 0;
}
diff --git a/otautil/SysUtil.cpp b/otautil/sysutil.cpp
similarity index 89%
rename from otautil/SysUtil.cpp
rename to otautil/sysutil.cpp
index 48336ad..d8969a0 100644
--- a/otautil/SysUtil.cpp
+++ b/otautil/sysutil.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
#include <errno.h> // TEMP_FAILURE_RETRY
#include <fcntl.h>
@@ -23,13 +23,16 @@
#include <sys/stat.h>
#include <sys/types.h>
+#include <algorithm>
#include <string>
#include <vector>
#include <android-base/file.h>
#include <android-base/logging.h>
+#include <android-base/properties.h>
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
+#include <cutils/android_reboot.h>
bool MemMapping::MapFD(int fd) {
struct stat sb;
@@ -201,3 +204,19 @@
};
ranges_.clear();
}
+
+bool reboot(const std::string& command) {
+ std::string cmd = command;
+ if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
+ cmd += ",quiescent";
+ }
+ return android::base::SetProperty(ANDROID_RB_PROPERTY, cmd);
+}
+
+std::vector<char*> StringVectorToNullTerminatedArray(const std::vector<std::string>& args) {
+ std::vector<char*> result(args.size());
+ std::transform(args.cbegin(), args.cend(), result.begin(),
+ [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+ result.push_back(nullptr);
+ return result;
+}
diff --git a/otautil/ThermalUtil.cpp b/otautil/thermalutil.cpp
similarity index 98%
rename from otautil/ThermalUtil.cpp
rename to otautil/thermalutil.cpp
index 5d9bd45..4660e05 100644
--- a/otautil/ThermalUtil.cpp
+++ b/otautil/thermalutil.cpp
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-#include "otautil/ThermalUtil.h"
+#include "otautil/thermalutil.h"
#include <dirent.h>
#include <stdio.h>
diff --git a/private/install.h b/private/install.h
index ef64bd4..7fdc741 100644
--- a/private/install.h
+++ b/private/install.h
@@ -23,9 +23,17 @@
#include <ziparchive/zip_archive.h>
-// Extract the update binary from the open zip archive |zip| located at |package| to |binary_path|.
-// Store the command line that should be called into |cmd|. The |status_fd| is the file descriptor
-// the child process should use to report back the progress of the update.
-int update_binary_command(const std::string& package, ZipArchiveHandle zip,
- const std::string& binary_path, int retry_count, int status_fd,
+// Sets up the commands for a non-A/B update. Extracts the updater binary from the open zip archive
+// |zip| located at |package|. Stores the command line that should be called into |cmd|. The
+// |status_fd| is the file descriptor the child process should use to report back the progress of
+// the update.
+int SetUpNonAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int retry_count,
+ int status_fd, std::vector<std::string>* cmd);
+
+// Sets up the commands for an A/B update. Extracts the needed entries from the open zip archive
+// |zip| located at |package|. Stores the command line that should be called into |cmd|. The
+// |status_fd| is the file descriptor the child process should use to report back the progress of
+// the update. Note that since this applies to the sideloading flow only, it takes one less
+// parameter |retry_count| than the non-A/B version.
+int SetUpAbUpdateCommands(const std::string& package, ZipArchiveHandle zip, int status_fd,
std::vector<std::string>* cmd);
diff --git a/recovery-persist.cpp b/recovery-persist.cpp
index dbce7ff..ebb42d2 100644
--- a/recovery-persist.cpp
+++ b/recovery-persist.cpp
@@ -35,19 +35,22 @@
#include <string.h>
#include <unistd.h>
+#include <limits>
#include <string>
#include <android-base/file.h>
#include <android-base/logging.h>
+#include <metricslogger/metrics_logger.h>
#include <private/android_logger.h> /* private pmsg functions */
-#include "rotate_logs.h"
+#include "logging.h"
+#include "otautil/parse_install_logs.h"
-static const char *LAST_LOG_FILE = "/data/misc/recovery/last_log";
-static const char *LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
-static const char *LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
-static const char *LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
-static const char *ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
+constexpr const char* LAST_LOG_FILE = "/data/misc/recovery/last_log";
+constexpr const char* LAST_PMSG_FILE = "/sys/fs/pstore/pmsg-ramoops-0";
+constexpr const char* LAST_KMSG_FILE = "/data/misc/recovery/last_kmsg";
+constexpr const char* LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops-0";
+constexpr const char* ALT_LAST_CONSOLE_FILE = "/sys/fs/pstore/console-ramoops";
// close a file, log an error if the error indicator is set
static void check_and_fclose(FILE *fp, const char *name) {
@@ -109,6 +112,20 @@
return android::base::WriteStringToFile(buffer, destination.c_str());
}
+// Parses the LAST_INSTALL file and reports the update metrics saved under recovery mode.
+static void report_metrics_from_last_install(const std::string& file_name) {
+ auto metrics = ParseLastInstall(file_name);
+ // TODO(xunchang) report the installation result.
+ for (const auto& [event, value] : metrics) {
+ if (value > std::numeric_limits<int>::max()) {
+ LOG(WARNING) << event << " (" << value << ") exceeds integer max.";
+ } else {
+ LOG(INFO) << "Uploading " << value << " to " << event;
+ android::metricslogger::LogHistogram(event, value);
+ }
+ }
+}
+
int main(int argc, char **argv) {
/* Is /cache a mount?, we have been delivered where we are not wanted */
@@ -138,14 +155,18 @@
}
if (has_cache) {
- /*
- * TBD: Future location to move content from
- * /cache/recovery to /data/misc/recovery/
- */
- /* if --force-persist flag, then transfer pmsg data anyways */
- if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
- return 0;
- }
+ // Collects and reports the non-a/b update metrics from last_install; and removes the file
+ // to avoid duplicate report.
+ report_metrics_from_last_install(LAST_INSTALL_FILE_IN_CACHE);
+ if (unlink(LAST_INSTALL_FILE_IN_CACHE) == -1) {
+ PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE_IN_CACHE;
+ }
+
+ // TBD: Future location to move content from /cache/recovery to /data/misc/recovery/
+ // if --force-persist flag, then transfer pmsg data anyways
+ if ((argc <= 1) || !argv[1] || strcmp(argv[1], "--force-persist")) {
+ return 0;
+ }
}
/* Is there something in pmsg? */
@@ -157,6 +178,15 @@
__android_log_pmsg_file_read(
LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", logsave, NULL);
+ // For those device without /cache, the last_install file has been copied to
+ // /data/misc/recovery from pmsg. Looks for the sideload history only.
+ if (!has_cache) {
+ report_metrics_from_last_install(LAST_INSTALL_FILE);
+ if (unlink(LAST_INSTALL_FILE) == -1) {
+ PLOG(ERROR) << "Failed to unlink " << LAST_INSTALL_FILE;
+ }
+ }
+
/* Is there a last console log too? */
if (rotated) {
if (!access(LAST_CONSOLE_FILE, R_OK)) {
diff --git a/recovery-refresh.cpp b/recovery-refresh.cpp
index 14565d3..aee1ca5 100644
--- a/recovery-refresh.cpp
+++ b/recovery-refresh.cpp
@@ -42,7 +42,7 @@
#include <private/android_logger.h> /* private pmsg functions */
-#include "rotate_logs.h"
+#include "logging.h"
int main(int argc, char **argv) {
static const char filter[] = "recovery/";
diff --git a/recovery.cpp b/recovery.cpp
index 07ec5cf..7cc344b 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+#include "recovery.h"
+
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
@@ -27,15 +29,13 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/klog.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
-#include <time.h>
#include <unistd.h>
#include <algorithm>
-#include <chrono>
+#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -48,96 +48,44 @@
#include <android-base/strings.h>
#include <android-base/unique_fd.h>
#include <bootloader_message/bootloader_message.h>
-#include <cutils/android_reboot.h>
#include <cutils/properties.h> /* for property_list */
-#include <health2/Health.h>
-#include <private/android_filesystem_config.h> /* for AID_SYSTEM */
-#include <private/android_logger.h> /* private pmsg functions */
-#include <selinux/android.h>
-#include <selinux/label.h>
-#include <selinux/selinux.h>
+#include <healthhalutils/HealthHalUtils.h>
#include <ziparchive/zip_archive.h>
#include "adb_install.h"
#include "common.h"
#include "device.h"
+#include "fsck_unshare_blocks.h"
#include "fuse_sdcard_provider.h"
#include "fuse_sideload.h"
#include "install.h"
-#include "minadbd/minadbd.h"
-#include "minui/minui.h"
-#include "otautil/DirUtil.h"
+#include "logging.h"
+#include "otautil/dirutil.h"
#include "otautil/error_code.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
#include "roots.h"
-#include "rotate_logs.h"
#include "screen_ui.h"
-#include "stub_ui.h"
#include "ui.h"
-static const struct option OPTIONS[] = {
- { "update_package", required_argument, NULL, 'u' },
- { "retry_count", required_argument, NULL, 'n' },
- { "wipe_data", no_argument, NULL, 'w' },
- { "wipe_cache", no_argument, NULL, 'c' },
- { "show_text", no_argument, NULL, 't' },
- { "sideload", no_argument, NULL, 's' },
- { "sideload_auto_reboot", no_argument, NULL, 'a' },
- { "just_exit", no_argument, NULL, 'x' },
- { "locale", required_argument, NULL, 'l' },
- { "shutdown_after", no_argument, NULL, 'p' },
- { "reason", required_argument, NULL, 'r' },
- { "security", no_argument, NULL, 'e'},
- { "wipe_ab", no_argument, NULL, 0 },
- { "wipe_package_size", required_argument, NULL, 0 },
- { "prompt_and_wipe_data", no_argument, NULL, 0 },
- { NULL, 0, NULL, 0 },
-};
+static constexpr const char* CACHE_LOG_DIR = "/cache/recovery";
+static constexpr const char* COMMAND_FILE = "/cache/recovery/command";
+static constexpr const char* LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
+static constexpr const char* LAST_LOG_FILE = "/cache/recovery/last_log";
+static constexpr const char* LOCALE_FILE = "/cache/recovery/last_locale";
-// More bootreasons can be found in "system/core/bootstat/bootstat.cpp".
-static const std::vector<std::string> bootreason_blacklist {
- "kernel_panic",
- "Panic",
-};
-
-static const char *CACHE_LOG_DIR = "/cache/recovery";
-static const char *COMMAND_FILE = "/cache/recovery/command";
-static const char *LOG_FILE = "/cache/recovery/log";
-static const char *LAST_INSTALL_FILE = "/cache/recovery/last_install";
-static const char *LOCALE_FILE = "/cache/recovery/last_locale";
-static const char *CONVERT_FBE_DIR = "/tmp/convert_fbe";
-static const char *CONVERT_FBE_FILE = "/tmp/convert_fbe/convert_fbe";
-static const char *CACHE_ROOT = "/cache";
-static const char *DATA_ROOT = "/data";
-static const char* METADATA_ROOT = "/metadata";
-static const char *SDCARD_ROOT = "/sdcard";
-static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
-static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
-static const char *LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
-static const char *LAST_LOG_FILE = "/cache/recovery/last_log";
-// We will try to apply the update package 5 times at most in case of an I/O error or
-// bspatch | imgpatch error.
-static const int RETRY_LIMIT = 4;
-static const int BATTERY_READ_TIMEOUT_IN_SEC = 10;
-// GmsCore enters recovery mode to install package when having enough battery
-// percentage. Normally, the threshold is 40% without charger and 20% with charger.
-// So we should check battery with a slightly lower limitation.
-static const int BATTERY_OK_PERCENTAGE = 20;
-static const int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
-static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
-static constexpr const char* DEFAULT_LOCALE = "en-US";
+static constexpr const char* CACHE_ROOT = "/cache";
+static constexpr const char* DATA_ROOT = "/data";
+static constexpr const char* METADATA_ROOT = "/metadata";
+static constexpr const char* SDCARD_ROOT = "/sdcard";
// We define RECOVERY_API_VERSION in Android.mk, which will be picked up by build system and packed
// into target_files.zip. Assert the version defined in code and in Android.mk are consistent.
static_assert(kRecoveryApiVersion == RECOVERY_API_VERSION, "Mismatching recovery API versions.");
-static std::string locale;
-static bool has_cache = false;
-
-RecoveryUI* ui = nullptr;
bool modified_flash = false;
std::string stage;
const char* reason = nullptr;
-struct selabel_handle* sehandle;
/*
* The recovery tool communicates with the main system through /cache files.
@@ -147,9 +95,10 @@
* The arguments which may be supplied in the recovery.command file:
* --update_package=path - verify install an OTA package file
* --wipe_data - erase user data (and cache), then reboot
- * --prompt_and_wipe_data - prompt the user that data is corrupt,
- * with their consent erase user data (and cache), then reboot
+ * --prompt_and_wipe_data - prompt the user that data is corrupt, with their consent erase user
+ * data (and cache), then reboot
* --wipe_cache - wipe cache (but not user data), then reboot
+ * --show_text - show the recovery text menu, used by some bootloader (e.g. http://b/36872519).
* --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
* --just_exit - do nothing; exit and reboot
*
@@ -184,202 +133,10 @@
* 7b. the user reboots (pulling the battery, etc) into the main system
*/
-// Open a given path, mounting partitions as necessary.
-FILE* fopen_path(const char* path, const char* mode) {
- if (ensure_path_mounted(path) != 0) {
- LOG(ERROR) << "Can't mount " << path;
- return nullptr;
- }
-
- // When writing, try to create the containing directory, if necessary. Use generous permissions,
- // the system (init.rc) will reset them.
- if (strchr("wa", mode[0])) {
- mkdir_recursively(path, 0777, true, sehandle);
- }
- return fopen(path, mode);
-}
-
-// close a file, log an error if the error indicator is set
-static void check_and_fclose(FILE *fp, const char *name) {
- fflush(fp);
- if (fsync(fileno(fp)) == -1) {
- PLOG(ERROR) << "Failed to fsync " << name;
- }
- if (ferror(fp)) {
- PLOG(ERROR) << "Error in " << name;
- }
- fclose(fp);
-}
-
bool is_ro_debuggable() {
return android::base::GetBoolProperty("ro.debuggable", false);
}
-bool reboot(const std::string& command) {
- std::string cmd = command;
- if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
- cmd += ",quiescent";
- }
- return android::base::SetProperty(ANDROID_RB_PROPERTY, cmd);
-}
-
-static void redirect_stdio(const char* filename) {
- int pipefd[2];
- if (pipe(pipefd) == -1) {
- PLOG(ERROR) << "pipe failed";
-
- // Fall back to traditional logging mode without timestamps.
- // If these fail, there's not really anywhere to complain...
- freopen(filename, "a", stdout); setbuf(stdout, NULL);
- freopen(filename, "a", stderr); setbuf(stderr, NULL);
-
- return;
- }
-
- pid_t pid = fork();
- if (pid == -1) {
- PLOG(ERROR) << "fork failed";
-
- // Fall back to traditional logging mode without timestamps.
- // If these fail, there's not really anywhere to complain...
- freopen(filename, "a", stdout); setbuf(stdout, NULL);
- freopen(filename, "a", stderr); setbuf(stderr, NULL);
-
- return;
- }
-
- if (pid == 0) {
- /// Close the unused write end.
- close(pipefd[1]);
-
- auto start = std::chrono::steady_clock::now();
-
- // Child logger to actually write to the log file.
- FILE* log_fp = fopen(filename, "ae");
- if (log_fp == nullptr) {
- PLOG(ERROR) << "fopen \"" << filename << "\" failed";
- close(pipefd[0]);
- _exit(EXIT_FAILURE);
- }
-
- FILE* pipe_fp = fdopen(pipefd[0], "r");
- if (pipe_fp == nullptr) {
- PLOG(ERROR) << "fdopen failed";
- check_and_fclose(log_fp, filename);
- close(pipefd[0]);
- _exit(EXIT_FAILURE);
- }
-
- char* line = nullptr;
- size_t len = 0;
- while (getline(&line, &len, pipe_fp) != -1) {
- auto now = std::chrono::steady_clock::now();
- double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
- now - start).count();
- if (line[0] == '\n') {
- fprintf(log_fp, "[%12.6lf]\n", duration);
- } else {
- fprintf(log_fp, "[%12.6lf] %s", duration, line);
- }
- fflush(log_fp);
- }
-
- PLOG(ERROR) << "getline failed";
-
- free(line);
- check_and_fclose(log_fp, filename);
- close(pipefd[0]);
- _exit(EXIT_FAILURE);
- } else {
- // Redirect stdout/stderr to the logger process.
- // Close the unused read end.
- close(pipefd[0]);
-
- setbuf(stdout, nullptr);
- setbuf(stderr, nullptr);
-
- if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
- PLOG(ERROR) << "dup2 stdout failed";
- }
- if (dup2(pipefd[1], STDERR_FILENO) == -1) {
- PLOG(ERROR) << "dup2 stderr failed";
- }
-
- close(pipefd[1]);
- }
-}
-
-// command line args come from, in decreasing precedence:
-// - the actual command line
-// - the bootloader control block (one per line, after "recovery")
-// - the contents of COMMAND_FILE (one per line)
-static std::vector<std::string> get_args(const int argc, char** const argv) {
- CHECK_GT(argc, 0);
-
- bootloader_message boot = {};
- std::string err;
- if (!read_bootloader_message(&boot, &err)) {
- LOG(ERROR) << err;
- // If fails, leave a zeroed bootloader_message.
- boot = {};
- }
- stage = std::string(boot.stage);
-
- if (boot.command[0] != 0) {
- std::string boot_command = std::string(boot.command, sizeof(boot.command));
- LOG(INFO) << "Boot command: " << boot_command;
- }
-
- if (boot.status[0] != 0) {
- std::string boot_status = std::string(boot.status, sizeof(boot.status));
- LOG(INFO) << "Boot status: " << boot_status;
- }
-
- std::vector<std::string> args(argv, argv + argc);
-
- // --- if arguments weren't supplied, look in the bootloader control block
- if (args.size() == 1) {
- boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
- std::string boot_recovery(boot.recovery);
- std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
- if (!tokens.empty() && tokens[0] == "recovery") {
- for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
- // Skip empty and '\0'-filled tokens.
- if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
- }
- LOG(INFO) << "Got " << args.size() << " arguments from boot message";
- } else if (boot.recovery[0] != 0) {
- LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
- }
- }
-
- // --- if that doesn't work, try the command file (if we have /cache).
- if (args.size() == 1 && has_cache) {
- std::string content;
- if (ensure_path_mounted(COMMAND_FILE) == 0 &&
- android::base::ReadFileToString(COMMAND_FILE, &content)) {
- std::vector<std::string> tokens = android::base::Split(content, "\n");
- // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
- // COMMAND_FILE doesn't use filename as the first argument).
- for (auto it = tokens.begin(); it != tokens.end(); it++) {
- // Skip empty and '\0'-filled tokens.
- if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
- }
- LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
- }
- }
-
- // Write the arguments (excluding the filename in args[0]) back into the
- // bootloader control block. So the device will always boot into recovery to
- // finish the pending work, until finish_recovery() is called.
- std::vector<std::string> options(args.cbegin() + 1, args.cend());
- if (!update_bootloader_message(options, &err)) {
- LOG(ERROR) << "Failed to set BCB message: " << err;
- }
-
- return args;
-}
-
// Set the BCB to reboot back into recovery (it won't resume the install from
// sdcard though).
static void set_sdcard_update_bootloader_message() {
@@ -390,103 +147,11 @@
}
}
-// Read from kernel log into buffer and write out to file.
-static void save_kernel_log(const char* destination) {
- int klog_buf_len = klogctl(KLOG_SIZE_BUFFER, 0, 0);
- if (klog_buf_len <= 0) {
- PLOG(ERROR) << "Error getting klog size";
- return;
- }
-
- std::string buffer(klog_buf_len, 0);
- int n = klogctl(KLOG_READ_ALL, &buffer[0], klog_buf_len);
- if (n == -1) {
- PLOG(ERROR) << "Error in reading klog";
- return;
- }
- buffer.resize(n);
- android::base::WriteStringToFile(buffer, destination);
-}
-
-// write content to the current pmsg session.
-static ssize_t __pmsg_write(const char *filename, const char *buf, size_t len) {
- return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
- filename, buf, len);
-}
-
-static void copy_log_file_to_pmsg(const char* source, const char* destination) {
- std::string content;
- android::base::ReadFileToString(source, &content);
- __pmsg_write(destination, content.c_str(), content.length());
-}
-
-// How much of the temp log we have copied to the copy in cache.
-static off_t tmplog_offset = 0;
-
-static void copy_log_file(const char* source, const char* destination, bool append) {
- FILE* dest_fp = fopen_path(destination, append ? "ae" : "we");
- if (dest_fp == nullptr) {
- PLOG(ERROR) << "Can't open " << destination;
- } else {
- FILE* source_fp = fopen(source, "re");
- if (source_fp != nullptr) {
- if (append) {
- fseeko(source_fp, tmplog_offset, SEEK_SET); // Since last write
- }
- char buf[4096];
- size_t bytes;
- while ((bytes = fread(buf, 1, sizeof(buf), source_fp)) != 0) {
- fwrite(buf, 1, bytes, dest_fp);
- }
- if (append) {
- tmplog_offset = ftello(source_fp);
- }
- check_and_fclose(source_fp, source);
- }
- check_and_fclose(dest_fp, destination);
- }
-}
-
-static void copy_logs() {
- // We only rotate and record the log of the current session if there are
- // actual attempts to modify the flash, such as wipes, installs from BCB
- // or menu selections. This is to avoid unnecessary rotation (and
- // possible deletion) of log files, if it does not do anything loggable.
- if (!modified_flash) {
- return;
- }
-
- // Always write to pmsg, this allows the OTA logs to be caught in logcat -L
- copy_log_file_to_pmsg(TEMPORARY_LOG_FILE, LAST_LOG_FILE);
- copy_log_file_to_pmsg(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE);
-
- // We can do nothing for now if there's no /cache partition.
- if (!has_cache) {
- return;
- }
-
- ensure_path_mounted(LAST_LOG_FILE);
- ensure_path_mounted(LAST_KMSG_FILE);
- rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
-
- // Copy logs to cache so the system can find out what happened.
- copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
- copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
- copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
- save_kernel_log(LAST_KMSG_FILE);
- chmod(LOG_FILE, 0600);
- chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
- chmod(LAST_KMSG_FILE, 0600);
- chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
- chmod(LAST_LOG_FILE, 0640);
- chmod(LAST_INSTALL_FILE, 0644);
- sync();
-}
-
// Clear the recovery command and prepare to boot a (hopefully working) system,
// copy our log file to cache as well (for the system to read). This function is
// idempotent: call it as many times as you like.
static void finish_recovery() {
+ std::string locale = ui->GetLocale();
// Save the locale to cache, so if recovery is next started up without a '--locale' argument
// (e.g., directly from the bootloader) it will use the last-known locale.
if (!locale.empty() && has_cache) {
@@ -498,7 +163,7 @@
}
}
- copy_logs();
+ copy_logs(modified_flash, has_cache);
// Reset to normal system boot so recovery won't cycle indefinitely.
std::string err;
@@ -575,18 +240,19 @@
ensure_path_unmounted(volume);
int result;
-
if (is_data && reason && strcmp(reason, "convert_fbe") == 0) {
- // Create convert_fbe breadcrumb file to signal to init
- // to convert to file based encryption, not full disk encryption
+ static constexpr const char* CONVERT_FBE_DIR = "/tmp/convert_fbe";
+ static constexpr const char* CONVERT_FBE_FILE = "/tmp/convert_fbe/convert_fbe";
+ // Create convert_fbe breadcrumb file to signal init to convert to file based encryption, not
+ // full disk encryption.
if (mkdir(CONVERT_FBE_DIR, 0700) != 0) {
- ui->Print("Failed to make convert_fbe dir %s\n", strerror(errno));
- return true;
+ PLOG(ERROR) << "Failed to mkdir " << CONVERT_FBE_DIR;
+ return false;
}
FILE* f = fopen(CONVERT_FBE_FILE, "wbe");
if (!f) {
- ui->Print("Failed to convert to file encryption %s\n", strerror(errno));
- return true;
+ PLOG(ERROR) << "Failed to convert to file encryption";
+ return false;
}
fclose(f);
result = format_volume(volume, CONVERT_FBE_DIR);
@@ -613,62 +279,17 @@
// Any part of the log we'd copied to cache is now gone.
// Reset the pointer so we copy from the beginning of the temp
// log.
- tmplog_offset = 0;
- copy_logs();
+ reset_tmplog_offset();
+ copy_logs(modified_flash, has_cache);
}
return (result == 0);
}
-// Display a menu with the specified 'headers' and 'items'. Device specific HandleMenuKey() may
-// return a positive number beyond the given range. Caller sets 'menu_only' to true to ensure only
-// a menu item gets selected. 'initial_selection' controls the initial cursor location. Returns the
-// (non-negative) chosen item number, or -1 if timed out waiting for input.
-static int get_menu_selection(const char* const* headers, const char* const* items, bool menu_only,
- int initial_selection, Device* device) {
- // Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
- ui->FlushKeys();
-
- ui->StartMenu(headers, items, initial_selection);
-
- int selected = initial_selection;
- int chosen_item = -1;
- while (chosen_item < 0) {
- int key = ui->WaitKey();
- if (key == -1) { // WaitKey() timed out.
- if (ui->WasTextEverVisible()) {
- continue;
- } else {
- LOG(INFO) << "Timed out waiting for key input; rebooting.";
- ui->EndMenu();
- return -1;
- }
- }
-
- bool visible = ui->IsTextVisible();
- int action = device->HandleMenuKey(key, visible);
-
- if (action < 0) {
- switch (action) {
- case Device::kHighlightUp:
- selected = ui->SelectMenu(--selected);
- break;
- case Device::kHighlightDown:
- selected = ui->SelectMenu(++selected);
- break;
- case Device::kInvokeItem:
- chosen_item = selected;
- break;
- case Device::kNoAction:
- break;
- }
- } else if (!menu_only) {
- chosen_item = action;
- }
- }
-
- ui->EndMenu();
- return chosen_item;
+// Sets the usb config to 'state'
+bool SetUsbConfig(const std::string& state) {
+ android::base::SetProperty("sys.usb.config", state);
+ return android::base::WaitForProperty("sys.usb.state", state);
}
// Returns the selected filename, or an empty string.
@@ -682,7 +303,7 @@
}
std::vector<std::string> dirs;
- std::vector<std::string> zips = { "../" }; // "../" is always the first entry.
+ std::vector<std::string> entries{ "../" }; // "../" is always the first entry.
dirent* de;
while ((de = readdir(d.get())) != nullptr) {
@@ -693,29 +314,30 @@
if (name == "." || name == "..") continue;
dirs.push_back(name + "/");
} else if (de->d_type == DT_REG && android::base::EndsWithIgnoreCase(name, ".zip")) {
- zips.push_back(name);
+ entries.push_back(name);
}
}
std::sort(dirs.begin(), dirs.end());
- std::sort(zips.begin(), zips.end());
+ std::sort(entries.begin(), entries.end());
- // Append dirs to the zips list.
- zips.insert(zips.end(), dirs.begin(), dirs.end());
+ // Append dirs to the entries list.
+ entries.insert(entries.end(), dirs.begin(), dirs.end());
- const char* entries[zips.size() + 1];
- entries[zips.size()] = nullptr;
- for (size_t i = 0; i < zips.size(); i++) {
- entries[i] = zips[i].c_str();
- }
+ std::vector<std::string> headers{ "Choose a package to install:", path };
- const char* headers[] = { "Choose a package to install:", path.c_str(), nullptr };
-
- int chosen_item = 0;
+ size_t chosen_item = 0;
while (true) {
- chosen_item = get_menu_selection(headers, entries, true, chosen_item, device);
+ chosen_item = ui->ShowMenu(
+ headers, entries, chosen_item, true,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
- const std::string& item = zips[chosen_item];
+ // Return if WaitKey() was interrupted.
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+ return "";
+ }
+
+ const std::string& item = entries[chosen_item];
if (chosen_item == 0) {
// Go up but continue browsing (if the caller is browse_directory).
return "";
@@ -737,15 +359,17 @@
}
static bool yes_no(Device* device, const char* question1, const char* question2) {
- const char* headers[] = { question1, question2, NULL };
- const char* items[] = { " No", " Yes", NULL };
+ std::vector<std::string> headers{ question1, question2 };
+ std::vector<std::string> items{ " No", " Yes" };
- int chosen_item = get_menu_selection(headers, items, true, 0, device);
- return (chosen_item == 1);
+ size_t chosen_item = ui->ShowMenu(
+ headers, items, 0, true,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+ return (chosen_item == 1);
}
static bool ask_to_wipe_data(Device* device) {
- return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!");
+ return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!");
}
// Return true on success.
@@ -770,27 +394,38 @@
return success;
}
-static bool prompt_and_wipe_data(Device* device) {
+static InstallResult prompt_and_wipe_data(Device* device) {
// Use a single string and let ScreenRecoveryUI handles the wrapping.
- const char* const headers[] = {
+ std::vector<std::string> headers{
"Can't load Android system. Your data may be corrupt. "
"If you continue to get this message, you may need to "
"perform a factory data reset and erase all user data "
"stored on this device.",
- nullptr
};
- const char* const items[] = {
+ // clang-format off
+ std::vector<std::string> items {
"Try again",
"Factory data reset",
- NULL
};
+ // clang-format on
for (;;) {
- int chosen_item = get_menu_selection(headers, items, true, 0, device);
+ size_t chosen_item = ui->ShowMenu(
+ headers, items, 0, true,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+ // If ShowMenu() returned RecoveryUI::KeyError::INTERRUPTED, WaitKey() was interrupted.
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+ return INSTALL_KEY_INTERRUPTED;
+ }
if (chosen_item != 1) {
- return 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;
+ }
}
}
}
@@ -913,34 +548,34 @@
return ota_type_matched && device_type_matched && (!has_serial_number || serial_number_matched);
}
-// Wipe the current A/B device, with a secure wipe of all the partitions in
-// RECOVERY_WIPE.
+// Wipes the current A/B device, with a secure wipe of all the partitions in RECOVERY_WIPE.
static bool wipe_ab_device(size_t wipe_package_size) {
- ui->SetBackground(RecoveryUI::ERASING);
- ui->SetProgressType(RecoveryUI::INDETERMINATE);
+ ui->SetBackground(RecoveryUI::ERASING);
+ ui->SetProgressType(RecoveryUI::INDETERMINATE);
- if (!check_wipe_package(wipe_package_size)) {
- LOG(ERROR) << "Failed to verify wipe package";
- return false;
- }
- std::string partition_list;
- if (!android::base::ReadFileToString(RECOVERY_WIPE, &partition_list)) {
- LOG(ERROR) << "failed to read \"" << RECOVERY_WIPE << "\"";
- return false;
+ if (!check_wipe_package(wipe_package_size)) {
+ LOG(ERROR) << "Failed to verify wipe package";
+ return false;
+ }
+ static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
+ std::string partition_list;
+ if (!android::base::ReadFileToString(RECOVERY_WIPE, &partition_list)) {
+ LOG(ERROR) << "failed to read \"" << RECOVERY_WIPE << "\"";
+ return false;
+ }
+
+ std::vector<std::string> lines = android::base::Split(partition_list, "\n");
+ for (const std::string& line : lines) {
+ std::string partition = android::base::Trim(line);
+ // Ignore '#' comment or empty lines.
+ if (android::base::StartsWith(partition, "#") || partition.empty()) {
+ continue;
}
- std::vector<std::string> lines = android::base::Split(partition_list, "\n");
- for (const std::string& line : lines) {
- std::string partition = android::base::Trim(line);
- // Ignore '#' comment or empty lines.
- if (android::base::StartsWith(partition, "#") || partition.empty()) {
- continue;
- }
-
- // Proceed anyway even if it fails to wipe some partition.
- secure_wipe_partition(partition);
- }
- return true;
+ // Proceed anyway even if it fails to wipe some partition.
+ secure_wipe_partition(partition);
+ }
+ return true;
}
static void choose_recovery_file(Device* device) {
@@ -966,28 +601,30 @@
}
} else {
// If cache partition is not found, view /tmp/recovery.log instead.
- if (access(TEMPORARY_LOG_FILE, R_OK) == -1) {
+ if (access(Paths::Get().temporary_log_file().c_str(), R_OK) == -1) {
return;
} else {
- entries.push_back(TEMPORARY_LOG_FILE);
+ entries.push_back(Paths::Get().temporary_log_file());
}
}
entries.push_back("Back");
- std::vector<const char*> menu_entries(entries.size());
- std::transform(entries.cbegin(), entries.cend(), menu_entries.begin(),
- [](const std::string& entry) { return entry.c_str(); });
- menu_entries.push_back(nullptr);
+ std::vector<std::string> headers{ "Select file to view" };
- const char* headers[] = { "Select file to view", nullptr };
-
- int chosen_item = 0;
+ size_t chosen_item = 0;
while (true) {
- chosen_item = get_menu_selection(headers, menu_entries.data(), true, chosen_item, device);
+ chosen_item = ui->ShowMenu(
+ headers, entries, chosen_item, true,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+
+ // Handle WaitKey() interrupt.
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+ break;
+ }
if (entries[chosen_item] == "Back") break;
- ui->ShowFile(entries[chosen_item].c_str());
+ ui->ShowFile(entries[chosen_item]);
}
}
@@ -1091,8 +728,7 @@
}
}
- result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache,
- TEMPORARY_INSTALL_FILE, false, 0/*retry_count*/);
+ result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0 /*retry_count*/);
break;
}
@@ -1131,12 +767,19 @@
}
ui->SetProgressType(RecoveryUI::EMPTY);
- int chosen_item = get_menu_selection(nullptr, device->GetMenuItems(), false, 0, device);
-
+ size_t chosen_item = ui->ShowMenu(
+ {}, device->GetMenuItems(), 0, false,
+ std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
+ // Handle Interrupt key
+ if (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED)) {
+ return Device::KEY_INTERRUPTED;
+ }
// Device-specific code may take some action here. It may return one of the core actions
// handled in the switch statement below.
Device::BuiltinAction chosen_action =
- (chosen_item == -1) ? Device::REBOOT : device->InvokeMenuItem(chosen_item);
+ (chosen_item == static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT))
+ ? Device::REBOOT
+ : device->InvokeMenuItem(chosen_item);
bool should_wipe_cache = false;
switch (chosen_action) {
@@ -1146,6 +789,8 @@
case Device::REBOOT:
case Device::SHUTDOWN:
case Device::REBOOT_BOOTLOADER:
+ case Device::ENTER_FASTBOOT:
+ case Device::ENTER_RECOVERY:
return chosen_action;
case Device::WIPE_DATA:
@@ -1169,7 +814,7 @@
{
bool adb = (chosen_action == Device::APPLY_ADB_SIDELOAD);
if (adb) {
- status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
+ status = apply_from_adb(&should_wipe_cache);
} else {
status = apply_from_sdcard(device, &should_wipe_cache);
}
@@ -1183,7 +828,7 @@
if (status != INSTALL_SUCCESS) {
ui->SetBackground(RecoveryUI::ERROR);
ui->Print("Installation aborted.\n");
- copy_logs();
+ copy_logs(modified_flash, has_cache);
} else if (!ui->IsTextVisible()) {
return Device::NO_ACTION; // reboot if logs aren't visible
} else {
@@ -1202,23 +847,24 @@
case Device::RUN_LOCALE_TEST: {
ScreenRecoveryUI* screen_ui = static_cast<ScreenRecoveryUI*>(ui);
- screen_ui->CheckBackgroundTextImages(locale);
+ screen_ui->CheckBackgroundTextImages();
break;
}
case Device::MOUNT_SYSTEM:
- // For a system image built with the root directory (i.e. system_root_image == "true"), we
- // mount it to /system_root, and symlink /system to /system_root/system to make adb shell
- // work (the symlink is created through the build system). (Bug: 22855115)
+ // the system partition is mounted at /mnt/system
if (android::base::GetBoolProperty("ro.build.system_root_image", false)) {
- if (ensure_path_mounted_at("/", "/system_root") != -1) {
+ if (ensure_path_mounted_at("/", "/mnt/system") != -1) {
ui->Print("Mounted /system.\n");
}
} else {
- if (ensure_path_mounted("/system") != -1) {
+ if (ensure_path_mounted_at("/system", "/mnt/system") != -1) {
ui->Print("Mounted /system.\n");
}
}
break;
+
+ case Device::KEY_INTERRUPTED:
+ return Device::KEY_INTERRUPTED;
}
}
}
@@ -1227,21 +873,6 @@
printf("%s=%s\n", key, name);
}
-static std::string load_locale_from_cache() {
- if (ensure_path_mounted(LOCALE_FILE) != 0) {
- LOG(ERROR) << "Can't mount " << LOCALE_FILE;
- return "";
- }
-
- std::string content;
- if (!android::base::ReadFileToString(LOCALE_FILE, &content)) {
- PLOG(ERROR) << "Can't read " << LOCALE_FILE;
- return "";
- }
-
- return android::base::Trim(content);
-}
-
void ui_print(const char* format, ...) {
std::string buffer;
va_list ap;
@@ -1256,55 +887,31 @@
}
}
-static constexpr char log_characters[] = "VDIWEF";
-
-void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
- const char* /* tag */, const char* /* file */, unsigned int /* line */,
- const char* message) {
- if (severity >= android::base::ERROR && ui != nullptr) {
- ui->Print("E:%s\n", message);
- } else {
- fprintf(stdout, "%c:%s\n", log_characters[severity], message);
- }
-}
-
-static bool is_battery_ok() {
+static bool is_battery_ok(int* required_battery_level) {
using android::hardware::health::V1_0::BatteryStatus;
+ using android::hardware::health::V2_0::get_health_service;
+ using android::hardware::health::V2_0::IHealth;
using android::hardware::health::V2_0::Result;
using android::hardware::health::V2_0::toString;
- using android::hardware::health::V2_0::implementation::Health;
- struct healthd_config healthd_config = {
- .batteryStatusPath = android::String8(android::String8::kEmptyString),
- .batteryHealthPath = android::String8(android::String8::kEmptyString),
- .batteryPresentPath = android::String8(android::String8::kEmptyString),
- .batteryCapacityPath = android::String8(android::String8::kEmptyString),
- .batteryVoltagePath = android::String8(android::String8::kEmptyString),
- .batteryTemperaturePath = android::String8(android::String8::kEmptyString),
- .batteryTechnologyPath = android::String8(android::String8::kEmptyString),
- .batteryCurrentNowPath = android::String8(android::String8::kEmptyString),
- .batteryCurrentAvgPath = android::String8(android::String8::kEmptyString),
- .batteryChargeCounterPath = android::String8(android::String8::kEmptyString),
- .batteryFullChargePath = android::String8(android::String8::kEmptyString),
- .batteryCycleCountPath = android::String8(android::String8::kEmptyString),
- .energyCounter = NULL,
- .boot_min_cap = 0,
- .screen_on = NULL
- };
+ android::sp<IHealth> health = get_health_service();
- auto health =
- android::hardware::health::V2_0::implementation::Health::initInstance(&healthd_config);
-
+ static constexpr int BATTERY_READ_TIMEOUT_IN_SEC = 10;
int wait_second = 0;
while (true) {
auto charge_status = BatteryStatus::UNKNOWN;
- health
- ->getChargeStatus([&charge_status](auto res, auto out_status) {
- if (res == Result::SUCCESS) {
- charge_status = out_status;
- }
- })
- .isOk(); // should not have transport error
+
+ if (health == nullptr) {
+ LOG(WARNING) << "no health implementation is found, assuming defaults";
+ } else {
+ health
+ ->getChargeStatus([&charge_status](auto res, auto out_status) {
+ if (res == Result::SUCCESS) {
+ charge_status = out_status;
+ }
+ })
+ .isOk(); // should not have transport error
+ }
// Treat unknown status as charged.
bool charged = (charge_status != BatteryStatus::DISCHARGING &&
@@ -1312,15 +919,17 @@
Result res = Result::UNKNOWN;
int32_t capacity = INT32_MIN;
- health
- ->getCapacity([&res, &capacity](auto out_res, auto out_capacity) {
- res = out_res;
- capacity = out_capacity;
- })
- .isOk(); // should not have transport error
+ if (health != nullptr) {
+ health
+ ->getCapacity([&res, &capacity](auto out_res, auto out_capacity) {
+ res = out_res;
+ capacity = out_capacity;
+ })
+ .isOk(); // should not have transport error
+ }
- ui_print("charge_status %d, charged %d, status %s, capacity %" PRId32 "\n", charge_status,
- charged, toString(res).c_str(), capacity);
+ LOG(INFO) << "charge_status " << toString(charge_status) << ", charged " << charged
+ << ", status " << toString(res) << ", capacity " << capacity;
// At startup, the battery drivers in devices like N5X/N6P take some time to load
// the battery profile. Before the load finishes, it reports value 50 as a fake
// capacity. BATTERY_READ_TIMEOUT_IN_SEC is set that the battery drivers are expected
@@ -1337,9 +946,15 @@
if (res != Result::SUCCESS) {
capacity = 100;
}
- return (charged && capacity >= BATTERY_WITH_CHARGER_OK_PERCENTAGE) ||
- (!charged && capacity >= BATTERY_OK_PERCENTAGE);
- }
+
+ // GmsCore enters recovery mode to install package when having enough battery percentage.
+ // Normally, the threshold is 40% without charger and 20% with charger. So we should check
+ // battery with a slightly lower limitation.
+ static constexpr int BATTERY_OK_PERCENTAGE = 20;
+ static constexpr int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
+ *required_battery_level = charged ? BATTERY_WITH_CHARGER_OK_PERCENTAGE : BATTERY_OK_PERCENTAGE;
+ return capacity >= *required_battery_level;
+ }
}
// Set the retry count to |retry_count| in BCB.
@@ -1362,71 +977,55 @@
static bool bootreason_in_blacklist() {
std::string bootreason = android::base::GetProperty("ro.boot.bootreason", "");
if (!bootreason.empty()) {
- for (const auto& str : bootreason_blacklist) {
- if (strcasecmp(str.c_str(), bootreason.c_str()) == 0) {
- return true;
- }
+ // More bootreasons can be found in "system/core/bootstat/bootstat.cpp".
+ static const std::vector<std::string> kBootreasonBlacklist{
+ "kernel_panic",
+ "Panic",
+ };
+ for (const auto& str : kBootreasonBlacklist) {
+ if (android::base::EqualsIgnoreCase(str, bootreason)) return true;
}
}
return false;
}
-static void log_failure_code(ErrorCode code, const char *update_package) {
- std::vector<std::string> log_buffer = {
- update_package,
- "0", // install result
- "error: " + std::to_string(code),
- };
- std::string log_content = android::base::Join(log_buffer, "\n");
- if (!android::base::WriteStringToFile(log_content, TEMPORARY_INSTALL_FILE)) {
- PLOG(ERROR) << "failed to write " << TEMPORARY_INSTALL_FILE;
- }
-
- // Also write the info into last_log.
- LOG(INFO) << log_content;
-}
-
-int main(int argc, char **argv) {
- // We don't have logcat yet under recovery; so we'll print error on screen and
- // log to stdout (which is redirected to recovery.log) as we used to do.
- android::base::InitLogging(argv, &UiLogger);
-
- // Take last pmsg contents and rewrite it to the current pmsg session.
- static const char filter[] = "recovery/";
- // Do we need to rotate?
- bool doRotate = false;
-
- __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &doRotate);
- // Take action to refresh pmsg contents
- __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &doRotate);
-
- // If this binary is started with the single argument "--adbd",
- // instead of being the normal recovery binary, it turns into kind
- // of a stripped-down version of adbd that only supports the
- // 'sideload' command. Note this must be a real argument, not
- // anything in the command file or bootloader control block; the
- // only way recovery should be run with this argument is when it
- // starts a copy of itself from the apply_from_adb() function.
- if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
- minadbd_main();
- return 0;
+static void log_failure_code(ErrorCode code, const std::string& update_package) {
+ std::vector<std::string> log_buffer = {
+ update_package,
+ "0", // install result
+ "error: " + std::to_string(code),
+ };
+ std::string log_content = android::base::Join(log_buffer, "\n");
+ const std::string& install_file = Paths::Get().temporary_install_file();
+ if (!android::base::WriteStringToFile(log_content, install_file)) {
+ PLOG(ERROR) << "Failed to write " << install_file;
}
- time_t start = time(nullptr);
+ // Also write the info into last_log.
+ LOG(INFO) << log_content;
+}
- // redirect_stdio should be called only in non-sideload mode. Otherwise
- // we may have two logger instances with different timestamps.
- redirect_stdio(TEMPORARY_LOG_FILE);
-
- printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
-
- load_volume_table();
- has_cache = volume_for_mount_point(CACHE_ROOT) != nullptr;
-
- std::vector<std::string> args = get_args(argc, argv);
- std::vector<char*> args_to_parse(args.size());
- std::transform(args.cbegin(), args.cend(), args_to_parse.begin(),
- [](const std::string& arg) { return const_cast<char*>(arg.c_str()); });
+Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args) {
+ static constexpr struct option OPTIONS[] = {
+ { "fastboot", no_argument, nullptr, 0 },
+ { "fsck_unshare_blocks", no_argument, nullptr, 0 },
+ { "just_exit", no_argument, nullptr, 'x' },
+ { "locale", required_argument, nullptr, 0 },
+ { "prompt_and_wipe_data", no_argument, nullptr, 0 },
+ { "reason", required_argument, nullptr, 0 },
+ { "retry_count", required_argument, nullptr, 0 },
+ { "security", no_argument, nullptr, 0 },
+ { "show_text", no_argument, nullptr, 't' },
+ { "shutdown_after", no_argument, nullptr, 0 },
+ { "sideload", no_argument, nullptr, 0 },
+ { "sideload_auto_reboot", no_argument, nullptr, 0 },
+ { "update_package", required_argument, nullptr, 0 },
+ { "wipe_ab", no_argument, nullptr, 0 },
+ { "wipe_cache", no_argument, nullptr, 0 },
+ { "wipe_data", no_argument, nullptr, 0 },
+ { "wipe_package_size", required_argument, nullptr, 0 },
+ { nullptr, 0, nullptr, 0 },
+ };
const char* update_package = nullptr;
bool should_wipe_data = false;
@@ -1434,64 +1033,62 @@
bool should_wipe_cache = false;
bool should_wipe_ab = false;
size_t wipe_package_size = 0;
- bool show_text = false;
bool sideload = false;
bool sideload_auto_reboot = false;
bool just_exit = false;
bool shutdown_after = false;
+ bool fsck_unshare_blocks = false;
int retry_count = 0;
bool security_update = false;
+ std::string locale;
+
+ auto args_to_parse = StringVectorToNullTerminatedArray(args);
int arg;
int option_index;
- while ((arg = getopt_long(args_to_parse.size(), args_to_parse.data(), "", OPTIONS,
+ // Parse everything before the last element (which must be a nullptr). getopt_long(3) expects a
+ // null-terminated char* array, but without counting null as an arg (i.e. argv[argc] should be
+ // nullptr).
+ while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,
&option_index)) != -1) {
switch (arg) {
- case 'n':
- android::base::ParseInt(optarg, &retry_count, 0);
- break;
- case 'u':
- update_package = optarg;
- break;
- case 'w':
- should_wipe_data = true;
- break;
- case 'c':
- should_wipe_cache = true;
- break;
case 't':
- show_text = true;
- break;
- case 's':
- sideload = true;
- break;
- case 'a':
- sideload = true;
- sideload_auto_reboot = true;
+ // Handled in recovery_main.cpp
break;
case 'x':
just_exit = true;
break;
- case 'l':
- locale = optarg;
- break;
- case 'p':
- shutdown_after = true;
- break;
- case 'r':
- reason = optarg;
- break;
- case 'e':
- security_update = true;
- break;
case 0: {
std::string option = OPTIONS[option_index].name;
- if (option == "wipe_ab") {
- should_wipe_ab = true;
- } else if (option == "wipe_package_size") {
- android::base::ParseUint(optarg, &wipe_package_size);
+ if (option == "fsck_unshare_blocks") {
+ fsck_unshare_blocks = true;
+ } else if (option == "locale" || option == "fastboot") {
+ // Handled in recovery_main.cpp
} else if (option == "prompt_and_wipe_data") {
should_prompt_and_wipe_data = true;
+ } else if (option == "reason") {
+ reason = optarg;
+ } else if (option == "retry_count") {
+ android::base::ParseInt(optarg, &retry_count, 0);
+ } else if (option == "security") {
+ security_update = true;
+ } else if (option == "sideload") {
+ sideload = true;
+ } else if (option == "sideload_auto_reboot") {
+ sideload = true;
+ sideload_auto_reboot = true;
+ } else if (option == "shutdown_after") {
+ shutdown_after = true;
+ } else if (option == "update_package") {
+ update_package = optarg;
+ } else if (option == "wipe_ab") {
+ should_wipe_ab = true;
+ } else if (option == "wipe_cache") {
+ should_wipe_cache = true;
+ } else if (option == "wipe_data") {
+ should_wipe_data = true;
+ } else if (option == "wipe_package_size") {
+ android::base::ParseUint(optarg, &wipe_package_size);
}
break;
}
@@ -1500,34 +1097,11 @@
continue;
}
}
+ optind = 1;
- if (locale.empty()) {
- if (has_cache) {
- locale = load_locale_from_cache();
- }
-
- if (locale.empty()) {
- locale = DEFAULT_LOCALE;
- }
- }
-
- printf("locale is [%s]\n", locale.c_str());
printf("stage is [%s]\n", stage.c_str());
printf("reason is [%s]\n", reason);
- Device* device = make_device();
- if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
- printf("Quiescent recovery mode.\n");
- ui = new StubRecoveryUI();
- } else {
- ui = device->GetUI();
-
- if (!ui->Init(locale)) {
- printf("Failed to initialize UI, use stub UI instead.\n");
- ui = new StubRecoveryUI();
- }
- }
-
// Set background string to "installing security update" for security update,
// otherwise set it to "installing system update".
ui->SetSystemUpdateText(security_update);
@@ -1537,15 +1111,12 @@
ui->SetStage(st_cur, st_max);
}
- ui->SetBackground(RecoveryUI::NONE);
- if (show_text) ui->ShowText(true);
+ std::vector<std::string> title_lines =
+ android::base::Split(android::base::GetProperty("ro.bootimage.build.fingerprint", ""), ":");
+ title_lines.insert(std::begin(title_lines), "Android Recovery");
+ ui->SetTitle(title_lines);
- sehandle = selinux_android_file_context_handle();
- selinux_android_set_sehandle(sehandle);
- if (!sehandle) {
- ui->Print("Warning: No file_contexts\n");
- }
-
+ ui->ResetKeyInterruptStatus();
device->StartRecovery();
printf("Command:");
@@ -1566,14 +1137,15 @@
// to log the update attempt since update_package is non-NULL.
modified_flash = true;
- if (!is_battery_ok()) {
- ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
- BATTERY_OK_PERCENTAGE);
+ int required_battery_level;
+ if (retry_count == 0 && !is_battery_ok(&required_battery_level)) {
+ ui->Print("battery capacity is not enough for installing package: %d%% needed\n",
+ required_battery_level);
// Log the error code to last_install when installation skips due to
// low battery.
log_failure_code(kLowBattery, update_package);
status = INSTALL_SKIPPED;
- } else if (bootreason_in_blacklist()) {
+ } else if (retry_count == 0 && bootreason_in_blacklist()) {
// Skip update-on-reboot when bootreason is kernel_panic or similar
ui->Print("bootreason is in the blacklist; skip OTA installation\n");
log_failure_code(kBootreasonInBlacklist, update_package);
@@ -1585,17 +1157,18 @@
set_retry_bootloader_message(retry_count + 1, args);
}
- status = install_package(update_package, &should_wipe_cache, TEMPORARY_INSTALL_FILE, true,
- retry_count);
+ status = install_package(update_package, &should_wipe_cache, true, retry_count);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
wipe_cache(false, device);
}
if (status != INSTALL_SUCCESS) {
ui->Print("Installation aborted.\n");
- // When I/O error happens, reboot and retry installation RETRY_LIMIT
- // times before we abandon this OTA update.
+
+ // When I/O error or bspatch/imgpatch error happens, reboot and retry installation
+ // RETRY_LIMIT times before we abandon this OTA update.
+ static constexpr int RETRY_LIMIT = 4;
if (status == INSTALL_RETRY && retry_count < RETRY_LIMIT) {
- copy_logs();
+ copy_logs(modified_flash, has_cache);
retry_count += 1;
set_retry_bootloader_message(retry_count, args);
// Print retry count on screen.
@@ -1623,12 +1196,15 @@
status = INSTALL_ERROR;
}
} else if (should_prompt_and_wipe_data) {
+ // Trigger the logging to capture the cause, even if user chooses to not wipe data.
+ modified_flash = true;
+
ui->ShowText(true);
ui->SetBackground(RecoveryUI::ERROR);
- if (!prompt_and_wipe_data(device)) {
- status = INSTALL_ERROR;
+ status = prompt_and_wipe_data(device);
+ if (status != INSTALL_KEY_INTERRUPTED) {
+ ui->ShowText(false);
}
- ui->ShowText(false);
} else if (should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
@@ -1647,7 +1223,7 @@
if (!sideload_auto_reboot) {
ui->ShowText(true);
}
- status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
+ status = apply_from_adb(&should_wipe_cache);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
@@ -1657,6 +1233,10 @@
if (sideload_auto_reboot) {
ui->Print("Rebooting automatically.\n");
}
+ } else if (fsck_unshare_blocks) {
+ if (!do_fsck_unshare_blocks()) {
+ status = INSTALL_ERROR;
+ }
} else if (!just_exit) {
// If this is an eng or userdebug build, automatically turn on the text display if no command
// is specified. Note that this should be called before setting the background to avoid
@@ -1693,25 +1273,5 @@
// Save logs and clean up before rebooting or shutting down.
finish_recovery();
- switch (after) {
- case Device::SHUTDOWN:
- ui->Print("Shutting down...\n");
- android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
- break;
-
- case Device::REBOOT_BOOTLOADER:
- ui->Print("Rebooting to bootloader...\n");
- android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
- break;
-
- default:
- ui->Print("Rebooting...\n");
- reboot("reboot,");
- break;
- }
- while (true) {
- pause();
- }
- // Should be unreachable.
- return EXIT_SUCCESS;
+ return after;
}
diff --git a/mounts.h b/recovery.h
similarity index 67%
copy from mounts.h
copy to recovery.h
index 0de1ebd..00e22da 100644
--- a/mounts.h
+++ b/recovery.h
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,11 @@
* limitations under the License.
*/
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+#pragma once
-struct MountedVolume;
+#include <string>
+#include <vector>
-bool scan_mounted_volumes();
+#include "device.h"
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+Device::BuiltinAction start_recovery(Device* device, const std::vector<std::string>& args);
diff --git a/recovery_main.cpp b/recovery_main.cpp
new file mode 100644
index 0000000..7835094
--- /dev/null
+++ b/recovery_main.cpp
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <dlfcn.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <linux/fs.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atomic>
+#include <string>
+#include <thread>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
+#include <bootloader_message/bootloader_message.h>
+#include <cutils/android_reboot.h>
+#include <cutils/sockets.h>
+#include <private/android_logger.h> /* private pmsg functions */
+#include <selinux/android.h>
+#include <selinux/label.h>
+#include <selinux/selinux.h>
+
+#include "common.h"
+#include "device.h"
+#include "fastboot/fastboot.h"
+#include "logging.h"
+#include "minadbd/minadbd.h"
+#include "otautil/paths.h"
+#include "otautil/sysutil.h"
+#include "recovery.h"
+#include "roots.h"
+#include "stub_ui.h"
+#include "ui.h"
+
+static constexpr const char* COMMAND_FILE = "/cache/recovery/command";
+static constexpr const char* LOCALE_FILE = "/cache/recovery/last_locale";
+
+static constexpr const char* CACHE_ROOT = "/cache";
+
+bool has_cache = false;
+
+RecoveryUI* ui = nullptr;
+struct selabel_handle* sehandle;
+
+static void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
+ const char* /* tag */, const char* /* file */, unsigned int /* line */,
+ const char* message) {
+ static constexpr char log_characters[] = "VDIWEF";
+ if (severity >= android::base::ERROR && ui != nullptr) {
+ ui->Print("E:%s\n", message);
+ } else {
+ fprintf(stdout, "%c:%s\n", log_characters[severity], message);
+ }
+}
+
+// command line args come from, in decreasing precedence:
+// - the actual command line
+// - the bootloader control block (one per line, after "recovery")
+// - the contents of COMMAND_FILE (one per line)
+static std::vector<std::string> get_args(const int argc, char** const argv) {
+ CHECK_GT(argc, 0);
+
+ bootloader_message boot = {};
+ std::string err;
+ if (!read_bootloader_message(&boot, &err)) {
+ LOG(ERROR) << err;
+ // If fails, leave a zeroed bootloader_message.
+ boot = {};
+ }
+ stage = std::string(boot.stage);
+
+ std::string boot_command;
+ if (boot.command[0] != 0) {
+ if (memchr(boot.command, '\0', sizeof(boot.command))) {
+ boot_command = std::string(boot.command);
+ } else {
+ boot_command = std::string(boot.command, sizeof(boot.command));
+ }
+ LOG(INFO) << "Boot command: " << boot_command;
+ }
+
+ if (boot.status[0] != 0) {
+ std::string boot_status = std::string(boot.status, sizeof(boot.status));
+ LOG(INFO) << "Boot status: " << boot_status;
+ }
+
+ std::vector<std::string> args(argv, argv + argc);
+
+ // --- if arguments weren't supplied, look in the bootloader control block
+ if (args.size() == 1) {
+ boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
+ std::string boot_recovery(boot.recovery);
+ std::vector<std::string> tokens = android::base::Split(boot_recovery, "\n");
+ if (!tokens.empty() && tokens[0] == "recovery") {
+ for (auto it = tokens.begin() + 1; it != tokens.end(); it++) {
+ // Skip empty and '\0'-filled tokens.
+ if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
+ }
+ LOG(INFO) << "Got " << args.size() << " arguments from boot message";
+ } else if (boot.recovery[0] != 0) {
+ LOG(ERROR) << "Bad boot message: \"" << boot_recovery << "\"";
+ }
+ }
+
+ // --- if that doesn't work, try the command file (if we have /cache).
+ if (args.size() == 1 && has_cache) {
+ std::string content;
+ if (ensure_path_mounted(COMMAND_FILE) == 0 &&
+ android::base::ReadFileToString(COMMAND_FILE, &content)) {
+ std::vector<std::string> tokens = android::base::Split(content, "\n");
+ // All the arguments in COMMAND_FILE are needed (unlike the BCB message,
+ // COMMAND_FILE doesn't use filename as the first argument).
+ for (auto it = tokens.begin(); it != tokens.end(); it++) {
+ // Skip empty and '\0'-filled tokens.
+ if (!it->empty() && (*it)[0] != '\0') args.push_back(std::move(*it));
+ }
+ LOG(INFO) << "Got " << args.size() << " arguments from " << COMMAND_FILE;
+ }
+ }
+
+ // Write the arguments (excluding the filename in args[0]) back into the
+ // bootloader control block. So the device will always boot into recovery to
+ // finish the pending work, until finish_recovery() is called.
+ std::vector<std::string> options(args.cbegin() + 1, args.cend());
+ if (!update_bootloader_message(options, &err)) {
+ LOG(ERROR) << "Failed to set BCB message: " << err;
+ }
+
+ // Finally, if no arguments were specified, check whether we should boot
+ // into fastboot.
+ if (args.size() == 1 && boot_command == "boot-fastboot") {
+ args.emplace_back("--fastboot");
+ }
+
+ return args;
+}
+
+static std::string load_locale_from_cache() {
+ if (ensure_path_mounted(LOCALE_FILE) != 0) {
+ LOG(ERROR) << "Can't mount " << LOCALE_FILE;
+ return "";
+ }
+
+ std::string content;
+ if (!android::base::ReadFileToString(LOCALE_FILE, &content)) {
+ PLOG(ERROR) << "Can't read " << LOCALE_FILE;
+ return "";
+ }
+
+ return android::base::Trim(content);
+}
+
+static void ListenRecoverySocket(RecoveryUI* ui, std::atomic<Device::BuiltinAction>& action) {
+ android::base::unique_fd sock_fd(android_get_control_socket("recovery"));
+ if (sock_fd < 0) {
+ PLOG(ERROR) << "Failed to open recovery socket";
+ return;
+ }
+ listen(sock_fd, 4);
+
+ while (true) {
+ android::base::unique_fd connection_fd;
+ connection_fd.reset(accept(sock_fd, nullptr, nullptr));
+ if (connection_fd < 0) {
+ PLOG(ERROR) << "Failed to accept socket connection";
+ continue;
+ }
+ char msg;
+ constexpr char kSwitchToFastboot = 'f';
+ constexpr char kSwitchToRecovery = 'r';
+ ssize_t ret = TEMP_FAILURE_RETRY(read(connection_fd, &msg, sizeof(msg)));
+ if (ret != sizeof(msg)) {
+ PLOG(ERROR) << "Couldn't read from socket";
+ continue;
+ }
+ switch (msg) {
+ case kSwitchToRecovery:
+ action = Device::BuiltinAction::ENTER_RECOVERY;
+ break;
+ case kSwitchToFastboot:
+ action = Device::BuiltinAction::ENTER_FASTBOOT;
+ break;
+ default:
+ LOG(ERROR) << "Unrecognized char from socket " << msg;
+ continue;
+ }
+ ui->InterruptKey();
+ }
+}
+
+static void redirect_stdio(const char* filename) {
+ int pipefd[2];
+ if (pipe(pipefd) == -1) {
+ PLOG(ERROR) << "pipe failed";
+
+ // Fall back to traditional logging mode without timestamps. If these fail, there's not really
+ // anywhere to complain...
+ freopen(filename, "a", stdout);
+ setbuf(stdout, nullptr);
+ freopen(filename, "a", stderr);
+ setbuf(stderr, nullptr);
+
+ return;
+ }
+
+ pid_t pid = fork();
+ if (pid == -1) {
+ PLOG(ERROR) << "fork failed";
+
+ // Fall back to traditional logging mode without timestamps. If these fail, there's not really
+ // anywhere to complain...
+ freopen(filename, "a", stdout);
+ setbuf(stdout, nullptr);
+ freopen(filename, "a", stderr);
+ setbuf(stderr, nullptr);
+
+ return;
+ }
+
+ if (pid == 0) {
+ /// Close the unused write end.
+ close(pipefd[1]);
+
+ auto start = std::chrono::steady_clock::now();
+
+ // Child logger to actually write to the log file.
+ FILE* log_fp = fopen(filename, "ae");
+ if (log_fp == nullptr) {
+ PLOG(ERROR) << "fopen \"" << filename << "\" failed";
+ close(pipefd[0]);
+ _exit(EXIT_FAILURE);
+ }
+
+ FILE* pipe_fp = fdopen(pipefd[0], "r");
+ if (pipe_fp == nullptr) {
+ PLOG(ERROR) << "fdopen failed";
+ check_and_fclose(log_fp, filename);
+ close(pipefd[0]);
+ _exit(EXIT_FAILURE);
+ }
+
+ char* line = nullptr;
+ size_t len = 0;
+ while (getline(&line, &len, pipe_fp) != -1) {
+ auto now = std::chrono::steady_clock::now();
+ double duration =
+ std::chrono::duration_cast<std::chrono::duration<double>>(now - start).count();
+ if (line[0] == '\n') {
+ fprintf(log_fp, "[%12.6lf]\n", duration);
+ } else {
+ fprintf(log_fp, "[%12.6lf] %s", duration, line);
+ }
+ fflush(log_fp);
+ }
+
+ PLOG(ERROR) << "getline failed";
+
+ free(line);
+ check_and_fclose(log_fp, filename);
+ close(pipefd[0]);
+ _exit(EXIT_FAILURE);
+ } else {
+ // Redirect stdout/stderr to the logger process. Close the unused read end.
+ close(pipefd[0]);
+
+ setbuf(stdout, nullptr);
+ setbuf(stderr, nullptr);
+
+ if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
+ PLOG(ERROR) << "dup2 stdout failed";
+ }
+ if (dup2(pipefd[1], STDERR_FILENO) == -1) {
+ PLOG(ERROR) << "dup2 stderr failed";
+ }
+
+ close(pipefd[1]);
+ }
+}
+
+int main(int argc, char** argv) {
+ // We don't have logcat yet under recovery; so we'll print error on screen and log to stdout
+ // (which is redirected to recovery.log) as we used to do.
+ android::base::InitLogging(argv, &UiLogger);
+
+ // Take last pmsg contents and rewrite it to the current pmsg session.
+ static constexpr const char filter[] = "recovery/";
+ // Do we need to rotate?
+ bool do_rotate = false;
+
+ __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &do_rotate);
+ // Take action to refresh pmsg contents
+ __android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &do_rotate);
+
+ // If this binary is started with the single argument "--adbd", instead of being the normal
+ // recovery binary, it turns into kind of a stripped-down version of adbd that only supports the
+ // 'sideload' command. Note this must be a real argument, not anything in the command file or
+ // bootloader control block; the only way recovery should be run with this argument is when it
+ // starts a copy of itself from the apply_from_adb() function.
+ if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
+ minadbd_main();
+ return 0;
+ }
+
+ time_t start = time(nullptr);
+
+ // redirect_stdio should be called only in non-sideload mode. Otherwise we may have two logger
+ // instances with different timestamps.
+ redirect_stdio(Paths::Get().temporary_log_file().c_str());
+
+ load_volume_table();
+ has_cache = volume_for_mount_point(CACHE_ROOT) != nullptr;
+
+ std::vector<std::string> args = get_args(argc, argv);
+ auto args_to_parse = StringVectorToNullTerminatedArray(args);
+
+ static constexpr struct option OPTIONS[] = {
+ { "fastboot", no_argument, nullptr, 0 },
+ { "locale", required_argument, nullptr, 0 },
+ { "show_text", no_argument, nullptr, 't' },
+ { nullptr, 0, nullptr, 0 },
+ };
+
+ bool show_text = false;
+ bool fastboot = false;
+ std::string locale;
+
+ int arg;
+ int option_index;
+ while ((arg = getopt_long(args_to_parse.size() - 1, args_to_parse.data(), "", OPTIONS,
+ &option_index)) != -1) {
+ switch (arg) {
+ case 't':
+ show_text = true;
+ break;
+ case 0: {
+ std::string option = OPTIONS[option_index].name;
+ if (option == "locale") {
+ locale = optarg;
+ } else if (option == "fastboot" &&
+ android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
+ fastboot = true;
+ }
+ break;
+ }
+ }
+ }
+ optind = 1;
+
+ if (locale.empty()) {
+ if (has_cache) {
+ locale = load_locale_from_cache();
+ }
+
+ if (locale.empty()) {
+ static constexpr const char* DEFAULT_LOCALE = "en-US";
+ locale = DEFAULT_LOCALE;
+ }
+ }
+
+ static constexpr const char* kDefaultLibRecoveryUIExt = "librecovery_ui_ext.so";
+ // Intentionally not calling dlclose(3) to avoid potential gotchas (e.g. `make_device` may have
+ // handed out pointers to code or static [or thread-local] data and doesn't collect them all back
+ // in on dlclose).
+ void* librecovery_ui_ext = dlopen(kDefaultLibRecoveryUIExt, RTLD_NOW);
+
+ using MakeDeviceType = decltype(&make_device);
+ MakeDeviceType make_device_func = nullptr;
+ if (librecovery_ui_ext == nullptr) {
+ printf("Failed to dlopen %s: %s\n", kDefaultLibRecoveryUIExt, dlerror());
+ } else {
+ reinterpret_cast<void*&>(make_device_func) = dlsym(librecovery_ui_ext, "make_device");
+ if (make_device_func == nullptr) {
+ printf("Failed to dlsym make_device: %s\n", dlerror());
+ }
+ }
+
+ Device* device;
+ if (make_device_func == nullptr) {
+ printf("Falling back to the default make_device() instead\n");
+ device = make_device();
+ } else {
+ printf("Loading make_device from %s\n", kDefaultLibRecoveryUIExt);
+ device = (*make_device_func)();
+ }
+
+ if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
+ printf("Quiescent recovery mode.\n");
+ device->ResetUI(new StubRecoveryUI());
+ } else {
+ if (!device->GetUI()->Init(locale)) {
+ printf("Failed to initialize UI; using stub UI instead.\n");
+ device->ResetUI(new StubRecoveryUI());
+ }
+ }
+ ui = device->GetUI();
+
+ if (!has_cache) {
+ device->RemoveMenuItemForAction(Device::WIPE_CACHE);
+ }
+
+ if (!android::base::GetBoolProperty("ro.boot.logical_partitions", false)) {
+ device->RemoveMenuItemForAction(Device::ENTER_FASTBOOT);
+ }
+
+ ui->SetBackground(RecoveryUI::NONE);
+ if (show_text) ui->ShowText(true);
+
+ LOG(INFO) << "Starting recovery (pid " << getpid() << ") on " << ctime(&start);
+ LOG(INFO) << "locale is [" << locale << "]";
+
+ sehandle = selinux_android_file_context_handle();
+ selinux_android_set_sehandle(sehandle);
+ if (!sehandle) {
+ ui->Print("Warning: No file_contexts\n");
+ }
+
+ std::atomic<Device::BuiltinAction> action;
+ std::thread listener_thread(ListenRecoverySocket, ui, std::ref(action));
+ listener_thread.detach();
+
+ while (true) {
+ std::string usb_config = fastboot ? "fastboot" : is_ro_debuggable() ? "adb" : "none";
+ std::string usb_state = android::base::GetProperty("sys.usb.state", "none");
+ if (usb_config != usb_state) {
+ if (!SetUsbConfig("none")) {
+ LOG(ERROR) << "Failed to clear USB config";
+ }
+ if (!SetUsbConfig(usb_config)) {
+ LOG(ERROR) << "Failed to set USB config to " << usb_config;
+ }
+ }
+
+ auto ret = fastboot ? StartFastboot(device, args) : start_recovery(device, args);
+
+ if (ret == Device::KEY_INTERRUPTED) {
+ ret = action.exchange(ret);
+ if (ret == Device::NO_ACTION) {
+ continue;
+ }
+ }
+ switch (ret) {
+ case Device::SHUTDOWN:
+ ui->Print("Shutting down...\n");
+ android::base::SetProperty(ANDROID_RB_PROPERTY, "shutdown,");
+ break;
+
+ case Device::REBOOT_BOOTLOADER:
+ ui->Print("Rebooting to bootloader...\n");
+ android::base::SetProperty(ANDROID_RB_PROPERTY, "reboot,bootloader");
+ break;
+
+ case Device::ENTER_FASTBOOT:
+ LOG(INFO) << "Entering fastboot";
+ fastboot = true;
+ break;
+
+ case Device::ENTER_RECOVERY:
+ LOG(INFO) << "Entering recovery";
+ fastboot = false;
+ break;
+
+ default:
+ ui->Print("Rebooting...\n");
+ reboot("reboot,");
+ break;
+ }
+ }
+
+ // Should be unreachable.
+ return EXIT_SUCCESS;
+}
diff --git a/res-560dpi b/res-560dpi
deleted file mode 120000
index 1db3a2e..0000000
--- a/res-560dpi
+++ /dev/null
@@ -1 +0,0 @@
-res-xxxhdpi
\ No newline at end of file
diff --git a/res-hdpi/images/loop00000.png b/res-hdpi/images/loop00000.png
index 9e9d1e3..7e50647 100644
--- a/res-hdpi/images/loop00000.png
+++ b/res-hdpi/images/loop00000.png
Binary files differ
diff --git a/res-hdpi/images/loop00001.png b/res-hdpi/images/loop00001.png
index cd53cc6..c8362df 100644
--- a/res-hdpi/images/loop00001.png
+++ b/res-hdpi/images/loop00001.png
Binary files differ
diff --git a/res-hdpi/images/loop00002.png b/res-hdpi/images/loop00002.png
index d5b5cea..d239bd0 100644
--- a/res-hdpi/images/loop00002.png
+++ b/res-hdpi/images/loop00002.png
Binary files differ
diff --git a/res-hdpi/images/loop00003.png b/res-hdpi/images/loop00003.png
index 50e08de..f42f5c4 100644
--- a/res-hdpi/images/loop00003.png
+++ b/res-hdpi/images/loop00003.png
Binary files differ
diff --git a/res-hdpi/images/loop00004.png b/res-hdpi/images/loop00004.png
index d69f762..9614452 100644
--- a/res-hdpi/images/loop00004.png
+++ b/res-hdpi/images/loop00004.png
Binary files differ
diff --git a/res-hdpi/images/loop00005.png b/res-hdpi/images/loop00005.png
index 32d368e..b30dc98 100644
--- a/res-hdpi/images/loop00005.png
+++ b/res-hdpi/images/loop00005.png
Binary files differ
diff --git a/res-hdpi/images/loop00006.png b/res-hdpi/images/loop00006.png
index fcc750b..b54969c 100644
--- a/res-hdpi/images/loop00006.png
+++ b/res-hdpi/images/loop00006.png
Binary files differ
diff --git a/res-hdpi/images/loop00007.png b/res-hdpi/images/loop00007.png
index d37ba5c..e338574 100644
--- a/res-hdpi/images/loop00007.png
+++ b/res-hdpi/images/loop00007.png
Binary files differ
diff --git a/res-hdpi/images/loop00008.png b/res-hdpi/images/loop00008.png
index 5a16054..85fa0f3 100644
--- a/res-hdpi/images/loop00008.png
+++ b/res-hdpi/images/loop00008.png
Binary files differ
diff --git a/res-hdpi/images/loop00009.png b/res-hdpi/images/loop00009.png
index 49ede64..d3cbf51 100644
--- a/res-hdpi/images/loop00009.png
+++ b/res-hdpi/images/loop00009.png
Binary files differ
diff --git a/res-hdpi/images/loop00010.png b/res-hdpi/images/loop00010.png
index f9e219f..daacc20 100644
--- a/res-hdpi/images/loop00010.png
+++ b/res-hdpi/images/loop00010.png
Binary files differ
diff --git a/res-hdpi/images/loop00011.png b/res-hdpi/images/loop00011.png
index 3fbe0b5..11224ab 100644
--- a/res-hdpi/images/loop00011.png
+++ b/res-hdpi/images/loop00011.png
Binary files differ
diff --git a/res-hdpi/images/loop00012.png b/res-hdpi/images/loop00012.png
index 3229461..3426440 100644
--- a/res-hdpi/images/loop00012.png
+++ b/res-hdpi/images/loop00012.png
Binary files differ
diff --git a/res-hdpi/images/loop00013.png b/res-hdpi/images/loop00013.png
index 69773ec..56875ef 100644
--- a/res-hdpi/images/loop00013.png
+++ b/res-hdpi/images/loop00013.png
Binary files differ
diff --git a/res-hdpi/images/loop00014.png b/res-hdpi/images/loop00014.png
index 56c15cc..9117dd2 100644
--- a/res-hdpi/images/loop00014.png
+++ b/res-hdpi/images/loop00014.png
Binary files differ
diff --git a/res-hdpi/images/loop00015.png b/res-hdpi/images/loop00015.png
index 2612681..a0b31d1 100644
--- a/res-hdpi/images/loop00015.png
+++ b/res-hdpi/images/loop00015.png
Binary files differ
diff --git a/res-hdpi/images/loop00016.png b/res-hdpi/images/loop00016.png
index 69f632e..9eafa7a 100644
--- a/res-hdpi/images/loop00016.png
+++ b/res-hdpi/images/loop00016.png
Binary files differ
diff --git a/res-hdpi/images/loop00017.png b/res-hdpi/images/loop00017.png
index af35615..869987c 100644
--- a/res-hdpi/images/loop00017.png
+++ b/res-hdpi/images/loop00017.png
Binary files differ
diff --git a/res-hdpi/images/loop00018.png b/res-hdpi/images/loop00018.png
index 0f72ff0..0172c1e 100644
--- a/res-hdpi/images/loop00018.png
+++ b/res-hdpi/images/loop00018.png
Binary files differ
diff --git a/res-hdpi/images/loop00019.png b/res-hdpi/images/loop00019.png
index f167644..c6db029 100644
--- a/res-hdpi/images/loop00019.png
+++ b/res-hdpi/images/loop00019.png
Binary files differ
diff --git a/res-hdpi/images/loop00020.png b/res-hdpi/images/loop00020.png
index 202a0fe..89197e2 100644
--- a/res-hdpi/images/loop00020.png
+++ b/res-hdpi/images/loop00020.png
Binary files differ
diff --git a/res-hdpi/images/loop00021.png b/res-hdpi/images/loop00021.png
index 8c102d9..52ad8ca 100644
--- a/res-hdpi/images/loop00021.png
+++ b/res-hdpi/images/loop00021.png
Binary files differ
diff --git a/res-hdpi/images/loop00022.png b/res-hdpi/images/loop00022.png
index 4bde99c..8aa0f7b 100644
--- a/res-hdpi/images/loop00022.png
+++ b/res-hdpi/images/loop00022.png
Binary files differ
diff --git a/res-hdpi/images/loop00023.png b/res-hdpi/images/loop00023.png
index 350acfb..e037ef8 100644
--- a/res-hdpi/images/loop00023.png
+++ b/res-hdpi/images/loop00023.png
Binary files differ
diff --git a/res-hdpi/images/loop00024.png b/res-hdpi/images/loop00024.png
index dde1a8e..12611c3 100644
--- a/res-hdpi/images/loop00024.png
+++ b/res-hdpi/images/loop00024.png
Binary files differ
diff --git a/res-hdpi/images/loop00025.png b/res-hdpi/images/loop00025.png
index a133ebd..dcc5b83 100644
--- a/res-hdpi/images/loop00025.png
+++ b/res-hdpi/images/loop00025.png
Binary files differ
diff --git a/res-hdpi/images/loop00026.png b/res-hdpi/images/loop00026.png
index 6825ad9..c2762cd 100644
--- a/res-hdpi/images/loop00026.png
+++ b/res-hdpi/images/loop00026.png
Binary files differ
diff --git a/res-hdpi/images/loop00027.png b/res-hdpi/images/loop00027.png
index 91bf1cf..a119d2c 100644
--- a/res-hdpi/images/loop00027.png
+++ b/res-hdpi/images/loop00027.png
Binary files differ
diff --git a/res-hdpi/images/loop00028.png b/res-hdpi/images/loop00028.png
index 8cba9bb..87c1fb5 100644
--- a/res-hdpi/images/loop00028.png
+++ b/res-hdpi/images/loop00028.png
Binary files differ
diff --git a/res-hdpi/images/loop00029.png b/res-hdpi/images/loop00029.png
index bd05993..e689c96 100644
--- a/res-hdpi/images/loop00029.png
+++ b/res-hdpi/images/loop00029.png
Binary files differ
diff --git a/res-hdpi/images/loop00030.png b/res-hdpi/images/loop00030.png
index e30821a..18615f8 100644
--- a/res-hdpi/images/loop00030.png
+++ b/res-hdpi/images/loop00030.png
Binary files differ
diff --git a/res-hdpi/images/loop00031.png b/res-hdpi/images/loop00031.png
index 4019860..ecc9cb3 100644
--- a/res-hdpi/images/loop00031.png
+++ b/res-hdpi/images/loop00031.png
Binary files differ
diff --git a/res-hdpi/images/loop00032.png b/res-hdpi/images/loop00032.png
index 41832bb..d3831ef 100644
--- a/res-hdpi/images/loop00032.png
+++ b/res-hdpi/images/loop00032.png
Binary files differ
diff --git a/res-hdpi/images/loop00033.png b/res-hdpi/images/loop00033.png
index 583f19c..ce123a0 100644
--- a/res-hdpi/images/loop00033.png
+++ b/res-hdpi/images/loop00033.png
Binary files differ
diff --git a/res-hdpi/images/loop00034.png b/res-hdpi/images/loop00034.png
index bffa72b..7168fb2 100644
--- a/res-hdpi/images/loop00034.png
+++ b/res-hdpi/images/loop00034.png
Binary files differ
diff --git a/res-hdpi/images/loop00035.png b/res-hdpi/images/loop00035.png
index d65d6b4..0713635 100644
--- a/res-hdpi/images/loop00035.png
+++ b/res-hdpi/images/loop00035.png
Binary files differ
diff --git a/res-hdpi/images/loop00036.png b/res-hdpi/images/loop00036.png
index a26cda1..55358db 100644
--- a/res-hdpi/images/loop00036.png
+++ b/res-hdpi/images/loop00036.png
Binary files differ
diff --git a/res-hdpi/images/loop00037.png b/res-hdpi/images/loop00037.png
index 660530d..430876c 100644
--- a/res-hdpi/images/loop00037.png
+++ b/res-hdpi/images/loop00037.png
Binary files differ
diff --git a/res-hdpi/images/loop00038.png b/res-hdpi/images/loop00038.png
index a3c9f31..1155b00 100644
--- a/res-hdpi/images/loop00038.png
+++ b/res-hdpi/images/loop00038.png
Binary files differ
diff --git a/res-hdpi/images/loop00039.png b/res-hdpi/images/loop00039.png
index 609d8ca..ea43a89 100644
--- a/res-hdpi/images/loop00039.png
+++ b/res-hdpi/images/loop00039.png
Binary files differ
diff --git a/res-hdpi/images/loop00040.png b/res-hdpi/images/loop00040.png
index 4190444..e9bbfcc 100644
--- a/res-hdpi/images/loop00040.png
+++ b/res-hdpi/images/loop00040.png
Binary files differ
diff --git a/res-hdpi/images/loop00041.png b/res-hdpi/images/loop00041.png
index 9c3c371..421db51 100644
--- a/res-hdpi/images/loop00041.png
+++ b/res-hdpi/images/loop00041.png
Binary files differ
diff --git a/res-hdpi/images/loop00042.png b/res-hdpi/images/loop00042.png
index dd5baae..91d3845 100644
--- a/res-hdpi/images/loop00042.png
+++ b/res-hdpi/images/loop00042.png
Binary files differ
diff --git a/res-hdpi/images/loop00043.png b/res-hdpi/images/loop00043.png
index 814724e..944f579 100644
--- a/res-hdpi/images/loop00043.png
+++ b/res-hdpi/images/loop00043.png
Binary files differ
diff --git a/res-hdpi/images/loop00044.png b/res-hdpi/images/loop00044.png
index 63c7392..2f61616 100644
--- a/res-hdpi/images/loop00044.png
+++ b/res-hdpi/images/loop00044.png
Binary files differ
diff --git a/res-hdpi/images/loop00045.png b/res-hdpi/images/loop00045.png
index 5c666ef..147a4e9 100644
--- a/res-hdpi/images/loop00045.png
+++ b/res-hdpi/images/loop00045.png
Binary files differ
diff --git a/res-hdpi/images/loop00046.png b/res-hdpi/images/loop00046.png
index 6fa4667..fb3ebe0 100644
--- a/res-hdpi/images/loop00046.png
+++ b/res-hdpi/images/loop00046.png
Binary files differ
diff --git a/res-hdpi/images/loop00047.png b/res-hdpi/images/loop00047.png
index 52537ea..437a743 100644
--- a/res-hdpi/images/loop00047.png
+++ b/res-hdpi/images/loop00047.png
Binary files differ
diff --git a/res-hdpi/images/loop00048.png b/res-hdpi/images/loop00048.png
index 412fd1c..b91328e 100644
--- a/res-hdpi/images/loop00048.png
+++ b/res-hdpi/images/loop00048.png
Binary files differ
diff --git a/res-hdpi/images/loop00049.png b/res-hdpi/images/loop00049.png
index 6cc8ef0..aa3a1f8 100644
--- a/res-hdpi/images/loop00049.png
+++ b/res-hdpi/images/loop00049.png
Binary files differ
diff --git a/res-hdpi/images/loop00050.png b/res-hdpi/images/loop00050.png
index caf36c5..5687d77 100644
--- a/res-hdpi/images/loop00050.png
+++ b/res-hdpi/images/loop00050.png
Binary files differ
diff --git a/res-hdpi/images/loop00051.png b/res-hdpi/images/loop00051.png
index 1cf8fb4..f54a5c0 100644
--- a/res-hdpi/images/loop00051.png
+++ b/res-hdpi/images/loop00051.png
Binary files differ
diff --git a/res-hdpi/images/loop00052.png b/res-hdpi/images/loop00052.png
index 7ee60e8..50eaadc 100644
--- a/res-hdpi/images/loop00052.png
+++ b/res-hdpi/images/loop00052.png
Binary files differ
diff --git a/res-hdpi/images/loop00053.png b/res-hdpi/images/loop00053.png
index 691bca0..033c7cc 100644
--- a/res-hdpi/images/loop00053.png
+++ b/res-hdpi/images/loop00053.png
Binary files differ
diff --git a/res-hdpi/images/loop00054.png b/res-hdpi/images/loop00054.png
index fa8d000..3d9fd89 100644
--- a/res-hdpi/images/loop00054.png
+++ b/res-hdpi/images/loop00054.png
Binary files differ
diff --git a/res-hdpi/images/loop00055.png b/res-hdpi/images/loop00055.png
index 3b7acb0..b24dc8f 100644
--- a/res-hdpi/images/loop00055.png
+++ b/res-hdpi/images/loop00055.png
Binary files differ
diff --git a/res-hdpi/images/loop00056.png b/res-hdpi/images/loop00056.png
index 1c94d30..9ddf436 100644
--- a/res-hdpi/images/loop00056.png
+++ b/res-hdpi/images/loop00056.png
Binary files differ
diff --git a/res-hdpi/images/loop00057.png b/res-hdpi/images/loop00057.png
index 703f48e..16218ac 100644
--- a/res-hdpi/images/loop00057.png
+++ b/res-hdpi/images/loop00057.png
Binary files differ
diff --git a/res-hdpi/images/loop00058.png b/res-hdpi/images/loop00058.png
index 8dae68a..24d5ee3 100644
--- a/res-hdpi/images/loop00058.png
+++ b/res-hdpi/images/loop00058.png
Binary files differ
diff --git a/res-hdpi/images/loop00059.png b/res-hdpi/images/loop00059.png
index 1105b43..480e73e 100644
--- a/res-hdpi/images/loop00059.png
+++ b/res-hdpi/images/loop00059.png
Binary files differ
diff --git a/res-hdpi/images/loop00060.png b/res-hdpi/images/loop00060.png
index 8ae4a86..2429f93 100644
--- a/res-hdpi/images/loop00060.png
+++ b/res-hdpi/images/loop00060.png
Binary files differ
diff --git a/res-hdpi/images/loop00061.png b/res-hdpi/images/loop00061.png
index c4fca2f..ec29add 100644
--- a/res-hdpi/images/loop00061.png
+++ b/res-hdpi/images/loop00061.png
Binary files differ
diff --git a/res-hdpi/images/loop00062.png b/res-hdpi/images/loop00062.png
index d59b9d4..a1aaae0 100644
--- a/res-hdpi/images/loop00062.png
+++ b/res-hdpi/images/loop00062.png
Binary files differ
diff --git a/res-hdpi/images/loop00063.png b/res-hdpi/images/loop00063.png
index 7ac8fdf..b567c08 100644
--- a/res-hdpi/images/loop00063.png
+++ b/res-hdpi/images/loop00063.png
Binary files differ
diff --git a/res-hdpi/images/loop00064.png b/res-hdpi/images/loop00064.png
index 1fa8fe8..995ae43 100644
--- a/res-hdpi/images/loop00064.png
+++ b/res-hdpi/images/loop00064.png
Binary files differ
diff --git a/res-hdpi/images/loop00065.png b/res-hdpi/images/loop00065.png
index 542ed34..26e9063 100644
--- a/res-hdpi/images/loop00065.png
+++ b/res-hdpi/images/loop00065.png
Binary files differ
diff --git a/res-hdpi/images/loop00066.png b/res-hdpi/images/loop00066.png
index 7b6af52..7eac08c 100644
--- a/res-hdpi/images/loop00066.png
+++ b/res-hdpi/images/loop00066.png
Binary files differ
diff --git a/res-hdpi/images/loop00067.png b/res-hdpi/images/loop00067.png
index 58d4fb7..c865a9a 100644
--- a/res-hdpi/images/loop00067.png
+++ b/res-hdpi/images/loop00067.png
Binary files differ
diff --git a/res-hdpi/images/loop00068.png b/res-hdpi/images/loop00068.png
index 1f1616e..2a9012d 100644
--- a/res-hdpi/images/loop00068.png
+++ b/res-hdpi/images/loop00068.png
Binary files differ
diff --git a/res-hdpi/images/loop00069.png b/res-hdpi/images/loop00069.png
index a2dbbfa..8d5bbde 100644
--- a/res-hdpi/images/loop00069.png
+++ b/res-hdpi/images/loop00069.png
Binary files differ
diff --git a/res-hdpi/images/loop00070.png b/res-hdpi/images/loop00070.png
index 60a345f..a01082c 100644
--- a/res-hdpi/images/loop00070.png
+++ b/res-hdpi/images/loop00070.png
Binary files differ
diff --git a/res-hdpi/images/loop00071.png b/res-hdpi/images/loop00071.png
index ac44427..ec5511e 100644
--- a/res-hdpi/images/loop00071.png
+++ b/res-hdpi/images/loop00071.png
Binary files differ
diff --git a/res-hdpi/images/loop00072.png b/res-hdpi/images/loop00072.png
index a9171eb..e3bc89b 100644
--- a/res-hdpi/images/loop00072.png
+++ b/res-hdpi/images/loop00072.png
Binary files differ
diff --git a/res-hdpi/images/loop00073.png b/res-hdpi/images/loop00073.png
index 7911d32..5ca64fc 100644
--- a/res-hdpi/images/loop00073.png
+++ b/res-hdpi/images/loop00073.png
Binary files differ
diff --git a/res-hdpi/images/loop00074.png b/res-hdpi/images/loop00074.png
index dcea580..44223ea 100644
--- a/res-hdpi/images/loop00074.png
+++ b/res-hdpi/images/loop00074.png
Binary files differ
diff --git a/res-hdpi/images/loop00075.png b/res-hdpi/images/loop00075.png
index 0a7a5a5..08582df 100644
--- a/res-hdpi/images/loop00075.png
+++ b/res-hdpi/images/loop00075.png
Binary files differ
diff --git a/res-hdpi/images/loop00076.png b/res-hdpi/images/loop00076.png
index 674c9d2..f84ecae 100644
--- a/res-hdpi/images/loop00076.png
+++ b/res-hdpi/images/loop00076.png
Binary files differ
diff --git a/res-hdpi/images/loop00077.png b/res-hdpi/images/loop00077.png
index e344f47..35a737c 100644
--- a/res-hdpi/images/loop00077.png
+++ b/res-hdpi/images/loop00077.png
Binary files differ
diff --git a/res-hdpi/images/loop00078.png b/res-hdpi/images/loop00078.png
index e0968ce..a8a38e2 100644
--- a/res-hdpi/images/loop00078.png
+++ b/res-hdpi/images/loop00078.png
Binary files differ
diff --git a/res-hdpi/images/loop00079.png b/res-hdpi/images/loop00079.png
index 2ff1fb0..f5f3eb4 100644
--- a/res-hdpi/images/loop00079.png
+++ b/res-hdpi/images/loop00079.png
Binary files differ
diff --git a/res-hdpi/images/loop00080.png b/res-hdpi/images/loop00080.png
index 26de5af..27d566b 100644
--- a/res-hdpi/images/loop00080.png
+++ b/res-hdpi/images/loop00080.png
Binary files differ
diff --git a/res-hdpi/images/loop00081.png b/res-hdpi/images/loop00081.png
index 1ef6cdd..65e475c 100644
--- a/res-hdpi/images/loop00081.png
+++ b/res-hdpi/images/loop00081.png
Binary files differ
diff --git a/res-hdpi/images/loop00082.png b/res-hdpi/images/loop00082.png
index 334874f..af03a6f 100644
--- a/res-hdpi/images/loop00082.png
+++ b/res-hdpi/images/loop00082.png
Binary files differ
diff --git a/res-hdpi/images/loop00083.png b/res-hdpi/images/loop00083.png
index 3b0deb1..5b7c260 100644
--- a/res-hdpi/images/loop00083.png
+++ b/res-hdpi/images/loop00083.png
Binary files differ
diff --git a/res-hdpi/images/loop00084.png b/res-hdpi/images/loop00084.png
index 4b8494c..7b6ed28 100644
--- a/res-hdpi/images/loop00084.png
+++ b/res-hdpi/images/loop00084.png
Binary files differ
diff --git a/res-hdpi/images/loop00085.png b/res-hdpi/images/loop00085.png
index 2e57027..bee4ad3 100644
--- a/res-hdpi/images/loop00085.png
+++ b/res-hdpi/images/loop00085.png
Binary files differ
diff --git a/res-hdpi/images/loop00086.png b/res-hdpi/images/loop00086.png
index ab6f437..2117acf 100644
--- a/res-hdpi/images/loop00086.png
+++ b/res-hdpi/images/loop00086.png
Binary files differ
diff --git a/res-hdpi/images/loop00087.png b/res-hdpi/images/loop00087.png
index d6c3dcd..ecf9ce4 100644
--- a/res-hdpi/images/loop00087.png
+++ b/res-hdpi/images/loop00087.png
Binary files differ
diff --git a/res-hdpi/images/loop00088.png b/res-hdpi/images/loop00088.png
index 88b3868..ede9daa 100644
--- a/res-hdpi/images/loop00088.png
+++ b/res-hdpi/images/loop00088.png
Binary files differ
diff --git a/res-hdpi/images/loop00089.png b/res-hdpi/images/loop00089.png
index 5b4551b..18fd86f 100644
--- a/res-hdpi/images/loop00089.png
+++ b/res-hdpi/images/loop00089.png
Binary files differ
diff --git a/res-hdpi/images/loop00090.png b/res-hdpi/images/loop00090.png
index 9e9d1e3..7e50647 100644
--- a/res-hdpi/images/loop00090.png
+++ b/res-hdpi/images/loop00090.png
Binary files differ
diff --git a/res-mdpi/images/loop00000.png b/res-mdpi/images/loop00000.png
index 0e11c01..7af53f8 100644
--- a/res-mdpi/images/loop00000.png
+++ b/res-mdpi/images/loop00000.png
Binary files differ
diff --git a/res-mdpi/images/loop00001.png b/res-mdpi/images/loop00001.png
index 9d87ecc..83cefe3 100644
--- a/res-mdpi/images/loop00001.png
+++ b/res-mdpi/images/loop00001.png
Binary files differ
diff --git a/res-mdpi/images/loop00002.png b/res-mdpi/images/loop00002.png
index 4a47986..c3eaa69 100644
--- a/res-mdpi/images/loop00002.png
+++ b/res-mdpi/images/loop00002.png
Binary files differ
diff --git a/res-mdpi/images/loop00003.png b/res-mdpi/images/loop00003.png
index 5e01eab..444d2d0 100644
--- a/res-mdpi/images/loop00003.png
+++ b/res-mdpi/images/loop00003.png
Binary files differ
diff --git a/res-mdpi/images/loop00004.png b/res-mdpi/images/loop00004.png
index cebf84a..d621b66 100644
--- a/res-mdpi/images/loop00004.png
+++ b/res-mdpi/images/loop00004.png
Binary files differ
diff --git a/res-mdpi/images/loop00005.png b/res-mdpi/images/loop00005.png
index 4d0e8b0..6054213 100644
--- a/res-mdpi/images/loop00005.png
+++ b/res-mdpi/images/loop00005.png
Binary files differ
diff --git a/res-mdpi/images/loop00006.png b/res-mdpi/images/loop00006.png
index 00f9543..cb8edea 100644
--- a/res-mdpi/images/loop00006.png
+++ b/res-mdpi/images/loop00006.png
Binary files differ
diff --git a/res-mdpi/images/loop00007.png b/res-mdpi/images/loop00007.png
index 9564221..cbab8f5 100644
--- a/res-mdpi/images/loop00007.png
+++ b/res-mdpi/images/loop00007.png
Binary files differ
diff --git a/res-mdpi/images/loop00008.png b/res-mdpi/images/loop00008.png
index 8d41cc5..a085c45 100644
--- a/res-mdpi/images/loop00008.png
+++ b/res-mdpi/images/loop00008.png
Binary files differ
diff --git a/res-mdpi/images/loop00009.png b/res-mdpi/images/loop00009.png
index 2761756..15ca20c 100644
--- a/res-mdpi/images/loop00009.png
+++ b/res-mdpi/images/loop00009.png
Binary files differ
diff --git a/res-mdpi/images/loop00010.png b/res-mdpi/images/loop00010.png
index d8b4865..722292d 100644
--- a/res-mdpi/images/loop00010.png
+++ b/res-mdpi/images/loop00010.png
Binary files differ
diff --git a/res-mdpi/images/loop00011.png b/res-mdpi/images/loop00011.png
index 8442353..3bc7e3e 100644
--- a/res-mdpi/images/loop00011.png
+++ b/res-mdpi/images/loop00011.png
Binary files differ
diff --git a/res-mdpi/images/loop00012.png b/res-mdpi/images/loop00012.png
index cb986c5..b9aa25d 100644
--- a/res-mdpi/images/loop00012.png
+++ b/res-mdpi/images/loop00012.png
Binary files differ
diff --git a/res-mdpi/images/loop00013.png b/res-mdpi/images/loop00013.png
index 63b89b2..5897586 100644
--- a/res-mdpi/images/loop00013.png
+++ b/res-mdpi/images/loop00013.png
Binary files differ
diff --git a/res-mdpi/images/loop00014.png b/res-mdpi/images/loop00014.png
index 9713813..7a16b25 100644
--- a/res-mdpi/images/loop00014.png
+++ b/res-mdpi/images/loop00014.png
Binary files differ
diff --git a/res-mdpi/images/loop00015.png b/res-mdpi/images/loop00015.png
index 3f666d7..6791773 100644
--- a/res-mdpi/images/loop00015.png
+++ b/res-mdpi/images/loop00015.png
Binary files differ
diff --git a/res-mdpi/images/loop00016.png b/res-mdpi/images/loop00016.png
index 3d76b04..15dea3a 100644
--- a/res-mdpi/images/loop00016.png
+++ b/res-mdpi/images/loop00016.png
Binary files differ
diff --git a/res-mdpi/images/loop00017.png b/res-mdpi/images/loop00017.png
index 1438d77..6e6db83 100644
--- a/res-mdpi/images/loop00017.png
+++ b/res-mdpi/images/loop00017.png
Binary files differ
diff --git a/res-mdpi/images/loop00018.png b/res-mdpi/images/loop00018.png
index c285fc6..2055ea9 100644
--- a/res-mdpi/images/loop00018.png
+++ b/res-mdpi/images/loop00018.png
Binary files differ
diff --git a/res-mdpi/images/loop00019.png b/res-mdpi/images/loop00019.png
index d6969ec..0c0d030 100644
--- a/res-mdpi/images/loop00019.png
+++ b/res-mdpi/images/loop00019.png
Binary files differ
diff --git a/res-mdpi/images/loop00020.png b/res-mdpi/images/loop00020.png
index 89aa012..58446e3 100644
--- a/res-mdpi/images/loop00020.png
+++ b/res-mdpi/images/loop00020.png
Binary files differ
diff --git a/res-mdpi/images/loop00021.png b/res-mdpi/images/loop00021.png
index b0bd514..398d9cc 100644
--- a/res-mdpi/images/loop00021.png
+++ b/res-mdpi/images/loop00021.png
Binary files differ
diff --git a/res-mdpi/images/loop00022.png b/res-mdpi/images/loop00022.png
index 684d023..068e8fa 100644
--- a/res-mdpi/images/loop00022.png
+++ b/res-mdpi/images/loop00022.png
Binary files differ
diff --git a/res-mdpi/images/loop00023.png b/res-mdpi/images/loop00023.png
index d008e98..240140d 100644
--- a/res-mdpi/images/loop00023.png
+++ b/res-mdpi/images/loop00023.png
Binary files differ
diff --git a/res-mdpi/images/loop00024.png b/res-mdpi/images/loop00024.png
index 8fe2185..26f0e7a 100644
--- a/res-mdpi/images/loop00024.png
+++ b/res-mdpi/images/loop00024.png
Binary files differ
diff --git a/res-mdpi/images/loop00025.png b/res-mdpi/images/loop00025.png
index c534bbd..9dbc038 100644
--- a/res-mdpi/images/loop00025.png
+++ b/res-mdpi/images/loop00025.png
Binary files differ
diff --git a/res-mdpi/images/loop00026.png b/res-mdpi/images/loop00026.png
index 61b11b5..a5bb811 100644
--- a/res-mdpi/images/loop00026.png
+++ b/res-mdpi/images/loop00026.png
Binary files differ
diff --git a/res-mdpi/images/loop00027.png b/res-mdpi/images/loop00027.png
index 5c01dfc..d915367 100644
--- a/res-mdpi/images/loop00027.png
+++ b/res-mdpi/images/loop00027.png
Binary files differ
diff --git a/res-mdpi/images/loop00028.png b/res-mdpi/images/loop00028.png
index c3e61c0..9ea4ac5 100644
--- a/res-mdpi/images/loop00028.png
+++ b/res-mdpi/images/loop00028.png
Binary files differ
diff --git a/res-mdpi/images/loop00029.png b/res-mdpi/images/loop00029.png
index e0b23ff..a9b1251 100644
--- a/res-mdpi/images/loop00029.png
+++ b/res-mdpi/images/loop00029.png
Binary files differ
diff --git a/res-mdpi/images/loop00030.png b/res-mdpi/images/loop00030.png
index 6618ef7..e3a6a2d 100644
--- a/res-mdpi/images/loop00030.png
+++ b/res-mdpi/images/loop00030.png
Binary files differ
diff --git a/res-mdpi/images/loop00031.png b/res-mdpi/images/loop00031.png
index dfde81e..6eb2d3a 100644
--- a/res-mdpi/images/loop00031.png
+++ b/res-mdpi/images/loop00031.png
Binary files differ
diff --git a/res-mdpi/images/loop00032.png b/res-mdpi/images/loop00032.png
index dc6a01e..18c540e 100644
--- a/res-mdpi/images/loop00032.png
+++ b/res-mdpi/images/loop00032.png
Binary files differ
diff --git a/res-mdpi/images/loop00033.png b/res-mdpi/images/loop00033.png
index 86d104b..2379ae1 100644
--- a/res-mdpi/images/loop00033.png
+++ b/res-mdpi/images/loop00033.png
Binary files differ
diff --git a/res-mdpi/images/loop00034.png b/res-mdpi/images/loop00034.png
index 07a6d7c..422154e 100644
--- a/res-mdpi/images/loop00034.png
+++ b/res-mdpi/images/loop00034.png
Binary files differ
diff --git a/res-mdpi/images/loop00035.png b/res-mdpi/images/loop00035.png
index 3e5cb4e..b57702a 100644
--- a/res-mdpi/images/loop00035.png
+++ b/res-mdpi/images/loop00035.png
Binary files differ
diff --git a/res-mdpi/images/loop00036.png b/res-mdpi/images/loop00036.png
index 6ac7e35..17f415a 100644
--- a/res-mdpi/images/loop00036.png
+++ b/res-mdpi/images/loop00036.png
Binary files differ
diff --git a/res-mdpi/images/loop00037.png b/res-mdpi/images/loop00037.png
index 527c48d..0482866 100644
--- a/res-mdpi/images/loop00037.png
+++ b/res-mdpi/images/loop00037.png
Binary files differ
diff --git a/res-mdpi/images/loop00038.png b/res-mdpi/images/loop00038.png
index 41c6a03..8a54faf 100644
--- a/res-mdpi/images/loop00038.png
+++ b/res-mdpi/images/loop00038.png
Binary files differ
diff --git a/res-mdpi/images/loop00039.png b/res-mdpi/images/loop00039.png
index d24d642..58810b1 100644
--- a/res-mdpi/images/loop00039.png
+++ b/res-mdpi/images/loop00039.png
Binary files differ
diff --git a/res-mdpi/images/loop00040.png b/res-mdpi/images/loop00040.png
index f3f077f..08e97c2 100644
--- a/res-mdpi/images/loop00040.png
+++ b/res-mdpi/images/loop00040.png
Binary files differ
diff --git a/res-mdpi/images/loop00041.png b/res-mdpi/images/loop00041.png
index 33e0715..9b6f522 100644
--- a/res-mdpi/images/loop00041.png
+++ b/res-mdpi/images/loop00041.png
Binary files differ
diff --git a/res-mdpi/images/loop00042.png b/res-mdpi/images/loop00042.png
index b1ef146..3d62e33 100644
--- a/res-mdpi/images/loop00042.png
+++ b/res-mdpi/images/loop00042.png
Binary files differ
diff --git a/res-mdpi/images/loop00043.png b/res-mdpi/images/loop00043.png
index d835f33..01b0bc1 100644
--- a/res-mdpi/images/loop00043.png
+++ b/res-mdpi/images/loop00043.png
Binary files differ
diff --git a/res-mdpi/images/loop00044.png b/res-mdpi/images/loop00044.png
index 47ee00f..3f97b24 100644
--- a/res-mdpi/images/loop00044.png
+++ b/res-mdpi/images/loop00044.png
Binary files differ
diff --git a/res-mdpi/images/loop00045.png b/res-mdpi/images/loop00045.png
index 2c9dd71..91a5a01 100644
--- a/res-mdpi/images/loop00045.png
+++ b/res-mdpi/images/loop00045.png
Binary files differ
diff --git a/res-mdpi/images/loop00046.png b/res-mdpi/images/loop00046.png
index 7b0a557..ceb43bf 100644
--- a/res-mdpi/images/loop00046.png
+++ b/res-mdpi/images/loop00046.png
Binary files differ
diff --git a/res-mdpi/images/loop00047.png b/res-mdpi/images/loop00047.png
index 60368fe..1cf282f 100644
--- a/res-mdpi/images/loop00047.png
+++ b/res-mdpi/images/loop00047.png
Binary files differ
diff --git a/res-mdpi/images/loop00048.png b/res-mdpi/images/loop00048.png
index 8da21b5..4977fe0 100644
--- a/res-mdpi/images/loop00048.png
+++ b/res-mdpi/images/loop00048.png
Binary files differ
diff --git a/res-mdpi/images/loop00049.png b/res-mdpi/images/loop00049.png
index 8604a17..2172b67 100644
--- a/res-mdpi/images/loop00049.png
+++ b/res-mdpi/images/loop00049.png
Binary files differ
diff --git a/res-mdpi/images/loop00050.png b/res-mdpi/images/loop00050.png
index 230ebd9..b23e707 100644
--- a/res-mdpi/images/loop00050.png
+++ b/res-mdpi/images/loop00050.png
Binary files differ
diff --git a/res-mdpi/images/loop00051.png b/res-mdpi/images/loop00051.png
index 3165ae8..4c0d71c 100644
--- a/res-mdpi/images/loop00051.png
+++ b/res-mdpi/images/loop00051.png
Binary files differ
diff --git a/res-mdpi/images/loop00052.png b/res-mdpi/images/loop00052.png
index bf43112..0077c74 100644
--- a/res-mdpi/images/loop00052.png
+++ b/res-mdpi/images/loop00052.png
Binary files differ
diff --git a/res-mdpi/images/loop00053.png b/res-mdpi/images/loop00053.png
index 7d801fa..aa8f669 100644
--- a/res-mdpi/images/loop00053.png
+++ b/res-mdpi/images/loop00053.png
Binary files differ
diff --git a/res-mdpi/images/loop00054.png b/res-mdpi/images/loop00054.png
index f3ee246..b362d3e 100644
--- a/res-mdpi/images/loop00054.png
+++ b/res-mdpi/images/loop00054.png
Binary files differ
diff --git a/res-mdpi/images/loop00055.png b/res-mdpi/images/loop00055.png
index fb9fcff..d8220fb 100644
--- a/res-mdpi/images/loop00055.png
+++ b/res-mdpi/images/loop00055.png
Binary files differ
diff --git a/res-mdpi/images/loop00056.png b/res-mdpi/images/loop00056.png
index f6b1ee7..b8a4dd7 100644
--- a/res-mdpi/images/loop00056.png
+++ b/res-mdpi/images/loop00056.png
Binary files differ
diff --git a/res-mdpi/images/loop00057.png b/res-mdpi/images/loop00057.png
index af009d1..5874b05 100644
--- a/res-mdpi/images/loop00057.png
+++ b/res-mdpi/images/loop00057.png
Binary files differ
diff --git a/res-mdpi/images/loop00058.png b/res-mdpi/images/loop00058.png
index 1cd550a..27753e0 100644
--- a/res-mdpi/images/loop00058.png
+++ b/res-mdpi/images/loop00058.png
Binary files differ
diff --git a/res-mdpi/images/loop00059.png b/res-mdpi/images/loop00059.png
index cf8d18c..6094b99 100644
--- a/res-mdpi/images/loop00059.png
+++ b/res-mdpi/images/loop00059.png
Binary files differ
diff --git a/res-mdpi/images/loop00060.png b/res-mdpi/images/loop00060.png
index cfa5384..0d2fa78 100644
--- a/res-mdpi/images/loop00060.png
+++ b/res-mdpi/images/loop00060.png
Binary files differ
diff --git a/res-mdpi/images/loop00061.png b/res-mdpi/images/loop00061.png
index 5fcbf47..7076ed8 100644
--- a/res-mdpi/images/loop00061.png
+++ b/res-mdpi/images/loop00061.png
Binary files differ
diff --git a/res-mdpi/images/loop00062.png b/res-mdpi/images/loop00062.png
index d360d24..6901f4c 100644
--- a/res-mdpi/images/loop00062.png
+++ b/res-mdpi/images/loop00062.png
Binary files differ
diff --git a/res-mdpi/images/loop00063.png b/res-mdpi/images/loop00063.png
index 7f59a66..a7579af 100644
--- a/res-mdpi/images/loop00063.png
+++ b/res-mdpi/images/loop00063.png
Binary files differ
diff --git a/res-mdpi/images/loop00064.png b/res-mdpi/images/loop00064.png
index e02809f..8486ff1 100644
--- a/res-mdpi/images/loop00064.png
+++ b/res-mdpi/images/loop00064.png
Binary files differ
diff --git a/res-mdpi/images/loop00065.png b/res-mdpi/images/loop00065.png
index 597e796..ac4acba 100644
--- a/res-mdpi/images/loop00065.png
+++ b/res-mdpi/images/loop00065.png
Binary files differ
diff --git a/res-mdpi/images/loop00066.png b/res-mdpi/images/loop00066.png
index 3f308f0..97e28fc 100644
--- a/res-mdpi/images/loop00066.png
+++ b/res-mdpi/images/loop00066.png
Binary files differ
diff --git a/res-mdpi/images/loop00067.png b/res-mdpi/images/loop00067.png
index 6435982..7c1e232 100644
--- a/res-mdpi/images/loop00067.png
+++ b/res-mdpi/images/loop00067.png
Binary files differ
diff --git a/res-mdpi/images/loop00068.png b/res-mdpi/images/loop00068.png
index 580790b..943d5f4 100644
--- a/res-mdpi/images/loop00068.png
+++ b/res-mdpi/images/loop00068.png
Binary files differ
diff --git a/res-mdpi/images/loop00069.png b/res-mdpi/images/loop00069.png
index ae2f4e8..3bd05c7 100644
--- a/res-mdpi/images/loop00069.png
+++ b/res-mdpi/images/loop00069.png
Binary files differ
diff --git a/res-mdpi/images/loop00070.png b/res-mdpi/images/loop00070.png
index 8240391..941b6c5 100644
--- a/res-mdpi/images/loop00070.png
+++ b/res-mdpi/images/loop00070.png
Binary files differ
diff --git a/res-mdpi/images/loop00071.png b/res-mdpi/images/loop00071.png
index 03f157c..be59796 100644
--- a/res-mdpi/images/loop00071.png
+++ b/res-mdpi/images/loop00071.png
Binary files differ
diff --git a/res-mdpi/images/loop00072.png b/res-mdpi/images/loop00072.png
index b62dfd0..dc4bac9 100644
--- a/res-mdpi/images/loop00072.png
+++ b/res-mdpi/images/loop00072.png
Binary files differ
diff --git a/res-mdpi/images/loop00073.png b/res-mdpi/images/loop00073.png
index ba746f2..b9167a7 100644
--- a/res-mdpi/images/loop00073.png
+++ b/res-mdpi/images/loop00073.png
Binary files differ
diff --git a/res-mdpi/images/loop00074.png b/res-mdpi/images/loop00074.png
index bafd213..20c2ee6 100644
--- a/res-mdpi/images/loop00074.png
+++ b/res-mdpi/images/loop00074.png
Binary files differ
diff --git a/res-mdpi/images/loop00075.png b/res-mdpi/images/loop00075.png
index fe1f3a4..6597e34 100644
--- a/res-mdpi/images/loop00075.png
+++ b/res-mdpi/images/loop00075.png
Binary files differ
diff --git a/res-mdpi/images/loop00076.png b/res-mdpi/images/loop00076.png
index 49960e5..48cafc0 100644
--- a/res-mdpi/images/loop00076.png
+++ b/res-mdpi/images/loop00076.png
Binary files differ
diff --git a/res-mdpi/images/loop00077.png b/res-mdpi/images/loop00077.png
index a112cb8..037542e 100644
--- a/res-mdpi/images/loop00077.png
+++ b/res-mdpi/images/loop00077.png
Binary files differ
diff --git a/res-mdpi/images/loop00078.png b/res-mdpi/images/loop00078.png
index 5d69ab8..26c8558 100644
--- a/res-mdpi/images/loop00078.png
+++ b/res-mdpi/images/loop00078.png
Binary files differ
diff --git a/res-mdpi/images/loop00079.png b/res-mdpi/images/loop00079.png
index 31f3b55..fbf8a82 100644
--- a/res-mdpi/images/loop00079.png
+++ b/res-mdpi/images/loop00079.png
Binary files differ
diff --git a/res-mdpi/images/loop00080.png b/res-mdpi/images/loop00080.png
index 42730be..e89a8e1 100644
--- a/res-mdpi/images/loop00080.png
+++ b/res-mdpi/images/loop00080.png
Binary files differ
diff --git a/res-mdpi/images/loop00081.png b/res-mdpi/images/loop00081.png
index 5ea003e..e818680 100644
--- a/res-mdpi/images/loop00081.png
+++ b/res-mdpi/images/loop00081.png
Binary files differ
diff --git a/res-mdpi/images/loop00082.png b/res-mdpi/images/loop00082.png
index ead6359..b31a67d 100644
--- a/res-mdpi/images/loop00082.png
+++ b/res-mdpi/images/loop00082.png
Binary files differ
diff --git a/res-mdpi/images/loop00083.png b/res-mdpi/images/loop00083.png
index 1d10991..0005c17 100644
--- a/res-mdpi/images/loop00083.png
+++ b/res-mdpi/images/loop00083.png
Binary files differ
diff --git a/res-mdpi/images/loop00084.png b/res-mdpi/images/loop00084.png
index 5aafdec..3297da9 100644
--- a/res-mdpi/images/loop00084.png
+++ b/res-mdpi/images/loop00084.png
Binary files differ
diff --git a/res-mdpi/images/loop00085.png b/res-mdpi/images/loop00085.png
index 6813c33..fc6b1c5 100644
--- a/res-mdpi/images/loop00085.png
+++ b/res-mdpi/images/loop00085.png
Binary files differ
diff --git a/res-mdpi/images/loop00086.png b/res-mdpi/images/loop00086.png
index 5d63072..29567f7 100644
--- a/res-mdpi/images/loop00086.png
+++ b/res-mdpi/images/loop00086.png
Binary files differ
diff --git a/res-mdpi/images/loop00087.png b/res-mdpi/images/loop00087.png
index 9c65826..d2882a4 100644
--- a/res-mdpi/images/loop00087.png
+++ b/res-mdpi/images/loop00087.png
Binary files differ
diff --git a/res-mdpi/images/loop00088.png b/res-mdpi/images/loop00088.png
index 6cb1bf0..8587c07 100644
--- a/res-mdpi/images/loop00088.png
+++ b/res-mdpi/images/loop00088.png
Binary files differ
diff --git a/res-mdpi/images/loop00089.png b/res-mdpi/images/loop00089.png
index b3d742d..77cbcb5 100644
--- a/res-mdpi/images/loop00089.png
+++ b/res-mdpi/images/loop00089.png
Binary files differ
diff --git a/res-mdpi/images/loop00090.png b/res-mdpi/images/loop00090.png
index 0e11c01..7af53f8 100644
--- a/res-mdpi/images/loop00090.png
+++ b/res-mdpi/images/loop00090.png
Binary files differ
diff --git a/res-xhdpi/images/loop00000.png b/res-xhdpi/images/loop00000.png
index b438e9e..0b95c09 100644
--- a/res-xhdpi/images/loop00000.png
+++ b/res-xhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xhdpi/images/loop00001.png b/res-xhdpi/images/loop00001.png
index 343a185..54b1a16 100644
--- a/res-xhdpi/images/loop00001.png
+++ b/res-xhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xhdpi/images/loop00002.png b/res-xhdpi/images/loop00002.png
index aa5bc61..4b39d3f 100644
--- a/res-xhdpi/images/loop00002.png
+++ b/res-xhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xhdpi/images/loop00003.png b/res-xhdpi/images/loop00003.png
index 5385340..d2a7a7e 100644
--- a/res-xhdpi/images/loop00003.png
+++ b/res-xhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xhdpi/images/loop00004.png b/res-xhdpi/images/loop00004.png
index cdead7d..fa40d42 100644
--- a/res-xhdpi/images/loop00004.png
+++ b/res-xhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xhdpi/images/loop00005.png b/res-xhdpi/images/loop00005.png
index 8eb502f..4ebd50a 100644
--- a/res-xhdpi/images/loop00005.png
+++ b/res-xhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xhdpi/images/loop00006.png b/res-xhdpi/images/loop00006.png
index 60b0f4a..b732001 100644
--- a/res-xhdpi/images/loop00006.png
+++ b/res-xhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xhdpi/images/loop00007.png b/res-xhdpi/images/loop00007.png
index a76c588..158f244 100644
--- a/res-xhdpi/images/loop00007.png
+++ b/res-xhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xhdpi/images/loop00008.png b/res-xhdpi/images/loop00008.png
index 80e1603..71b2bd5 100644
--- a/res-xhdpi/images/loop00008.png
+++ b/res-xhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xhdpi/images/loop00009.png b/res-xhdpi/images/loop00009.png
index b8f4954..c92db12 100644
--- a/res-xhdpi/images/loop00009.png
+++ b/res-xhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xhdpi/images/loop00010.png b/res-xhdpi/images/loop00010.png
index b58d6ac..0a76222 100644
--- a/res-xhdpi/images/loop00010.png
+++ b/res-xhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xhdpi/images/loop00011.png b/res-xhdpi/images/loop00011.png
index 0b67f37..c26ed41 100644
--- a/res-xhdpi/images/loop00011.png
+++ b/res-xhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xhdpi/images/loop00012.png b/res-xhdpi/images/loop00012.png
index 234d77a..9f4405b 100644
--- a/res-xhdpi/images/loop00012.png
+++ b/res-xhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xhdpi/images/loop00013.png b/res-xhdpi/images/loop00013.png
index 13c6524..2d325e2 100644
--- a/res-xhdpi/images/loop00013.png
+++ b/res-xhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xhdpi/images/loop00014.png b/res-xhdpi/images/loop00014.png
index 92e30e3..d5328e2 100644
--- a/res-xhdpi/images/loop00014.png
+++ b/res-xhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xhdpi/images/loop00015.png b/res-xhdpi/images/loop00015.png
index 9c6076d..8c37522 100644
--- a/res-xhdpi/images/loop00015.png
+++ b/res-xhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xhdpi/images/loop00016.png b/res-xhdpi/images/loop00016.png
index 6f626c0..e3a881b 100644
--- a/res-xhdpi/images/loop00016.png
+++ b/res-xhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xhdpi/images/loop00017.png b/res-xhdpi/images/loop00017.png
index ff67d5b..8f6a906 100644
--- a/res-xhdpi/images/loop00017.png
+++ b/res-xhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xhdpi/images/loop00018.png b/res-xhdpi/images/loop00018.png
index 67b5d8f..e566b4a 100644
--- a/res-xhdpi/images/loop00018.png
+++ b/res-xhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xhdpi/images/loop00019.png b/res-xhdpi/images/loop00019.png
index 06ca980..66996a6 100644
--- a/res-xhdpi/images/loop00019.png
+++ b/res-xhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xhdpi/images/loop00020.png b/res-xhdpi/images/loop00020.png
index c2288b4..d2fdde3 100644
--- a/res-xhdpi/images/loop00020.png
+++ b/res-xhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xhdpi/images/loop00021.png b/res-xhdpi/images/loop00021.png
index ba5df46..01064ab 100644
--- a/res-xhdpi/images/loop00021.png
+++ b/res-xhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xhdpi/images/loop00022.png b/res-xhdpi/images/loop00022.png
index 2b1e947..a2de17b 100644
--- a/res-xhdpi/images/loop00022.png
+++ b/res-xhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xhdpi/images/loop00023.png b/res-xhdpi/images/loop00023.png
index 292e074..2bbea09 100644
--- a/res-xhdpi/images/loop00023.png
+++ b/res-xhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xhdpi/images/loop00024.png b/res-xhdpi/images/loop00024.png
index 11352f6..787b97f 100644
--- a/res-xhdpi/images/loop00024.png
+++ b/res-xhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xhdpi/images/loop00025.png b/res-xhdpi/images/loop00025.png
index 4212c76..7f37ed7 100644
--- a/res-xhdpi/images/loop00025.png
+++ b/res-xhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xhdpi/images/loop00026.png b/res-xhdpi/images/loop00026.png
index 774d00f..2ffa399 100644
--- a/res-xhdpi/images/loop00026.png
+++ b/res-xhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xhdpi/images/loop00027.png b/res-xhdpi/images/loop00027.png
index 1827471..0f9676c 100644
--- a/res-xhdpi/images/loop00027.png
+++ b/res-xhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xhdpi/images/loop00028.png b/res-xhdpi/images/loop00028.png
index f4e79f9..fd4b45d 100644
--- a/res-xhdpi/images/loop00028.png
+++ b/res-xhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xhdpi/images/loop00029.png b/res-xhdpi/images/loop00029.png
index 8638500..820c789 100644
--- a/res-xhdpi/images/loop00029.png
+++ b/res-xhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xhdpi/images/loop00030.png b/res-xhdpi/images/loop00030.png
index 94fd376..0307431 100644
--- a/res-xhdpi/images/loop00030.png
+++ b/res-xhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xhdpi/images/loop00031.png b/res-xhdpi/images/loop00031.png
index 441a52d..eebbc65 100644
--- a/res-xhdpi/images/loop00031.png
+++ b/res-xhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xhdpi/images/loop00032.png b/res-xhdpi/images/loop00032.png
index a10598f..b05284a 100644
--- a/res-xhdpi/images/loop00032.png
+++ b/res-xhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xhdpi/images/loop00033.png b/res-xhdpi/images/loop00033.png
index 96bf453..cf74124 100644
--- a/res-xhdpi/images/loop00033.png
+++ b/res-xhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xhdpi/images/loop00034.png b/res-xhdpi/images/loop00034.png
index 59baf8c..189b88a 100644
--- a/res-xhdpi/images/loop00034.png
+++ b/res-xhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xhdpi/images/loop00035.png b/res-xhdpi/images/loop00035.png
index 400a895..35b2fd2 100644
--- a/res-xhdpi/images/loop00035.png
+++ b/res-xhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xhdpi/images/loop00036.png b/res-xhdpi/images/loop00036.png
index fda7acc..5d156b2 100644
--- a/res-xhdpi/images/loop00036.png
+++ b/res-xhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xhdpi/images/loop00037.png b/res-xhdpi/images/loop00037.png
index d474e6f..08edb60 100644
--- a/res-xhdpi/images/loop00037.png
+++ b/res-xhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xhdpi/images/loop00038.png b/res-xhdpi/images/loop00038.png
index c5632e1..0e4cb0e 100644
--- a/res-xhdpi/images/loop00038.png
+++ b/res-xhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xhdpi/images/loop00039.png b/res-xhdpi/images/loop00039.png
index 3cf8b86..0671829 100644
--- a/res-xhdpi/images/loop00039.png
+++ b/res-xhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xhdpi/images/loop00040.png b/res-xhdpi/images/loop00040.png
index ef55a92..c0f602f 100644
--- a/res-xhdpi/images/loop00040.png
+++ b/res-xhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xhdpi/images/loop00041.png b/res-xhdpi/images/loop00041.png
index 60bf780..84928df 100644
--- a/res-xhdpi/images/loop00041.png
+++ b/res-xhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xhdpi/images/loop00042.png b/res-xhdpi/images/loop00042.png
index cee6980..131b316 100644
--- a/res-xhdpi/images/loop00042.png
+++ b/res-xhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xhdpi/images/loop00043.png b/res-xhdpi/images/loop00043.png
index fe5abc1..7ef0e8f 100644
--- a/res-xhdpi/images/loop00043.png
+++ b/res-xhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xhdpi/images/loop00044.png b/res-xhdpi/images/loop00044.png
index f33fcee..5fde006 100644
--- a/res-xhdpi/images/loop00044.png
+++ b/res-xhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xhdpi/images/loop00045.png b/res-xhdpi/images/loop00045.png
index e61b2a0..961459e 100644
--- a/res-xhdpi/images/loop00045.png
+++ b/res-xhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xhdpi/images/loop00046.png b/res-xhdpi/images/loop00046.png
index 4d919c0..a4610d6 100644
--- a/res-xhdpi/images/loop00046.png
+++ b/res-xhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xhdpi/images/loop00047.png b/res-xhdpi/images/loop00047.png
index deaf9a3..484becb 100644
--- a/res-xhdpi/images/loop00047.png
+++ b/res-xhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xhdpi/images/loop00048.png b/res-xhdpi/images/loop00048.png
index 82d8b2b..44b342d 100644
--- a/res-xhdpi/images/loop00048.png
+++ b/res-xhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xhdpi/images/loop00049.png b/res-xhdpi/images/loop00049.png
index a310cc9..7e96338 100644
--- a/res-xhdpi/images/loop00049.png
+++ b/res-xhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xhdpi/images/loop00050.png b/res-xhdpi/images/loop00050.png
index ad80230..3121bbe 100644
--- a/res-xhdpi/images/loop00050.png
+++ b/res-xhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xhdpi/images/loop00051.png b/res-xhdpi/images/loop00051.png
index 52f1ce6..2ea6784 100644
--- a/res-xhdpi/images/loop00051.png
+++ b/res-xhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xhdpi/images/loop00052.png b/res-xhdpi/images/loop00052.png
index c579e87..2b9b11a 100644
--- a/res-xhdpi/images/loop00052.png
+++ b/res-xhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xhdpi/images/loop00053.png b/res-xhdpi/images/loop00053.png
index 2c1bc91..716f8a1 100644
--- a/res-xhdpi/images/loop00053.png
+++ b/res-xhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xhdpi/images/loop00054.png b/res-xhdpi/images/loop00054.png
index 8885475..120adc3 100644
--- a/res-xhdpi/images/loop00054.png
+++ b/res-xhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xhdpi/images/loop00055.png b/res-xhdpi/images/loop00055.png
index 00d67da..257bfa4 100644
--- a/res-xhdpi/images/loop00055.png
+++ b/res-xhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xhdpi/images/loop00056.png b/res-xhdpi/images/loop00056.png
index 00ad26a..45467b7 100644
--- a/res-xhdpi/images/loop00056.png
+++ b/res-xhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xhdpi/images/loop00057.png b/res-xhdpi/images/loop00057.png
index 3511795..41a96fc 100644
--- a/res-xhdpi/images/loop00057.png
+++ b/res-xhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xhdpi/images/loop00058.png b/res-xhdpi/images/loop00058.png
index 9d28f7d..36ecf64 100644
--- a/res-xhdpi/images/loop00058.png
+++ b/res-xhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xhdpi/images/loop00059.png b/res-xhdpi/images/loop00059.png
index 776f40e..443c0ce 100644
--- a/res-xhdpi/images/loop00059.png
+++ b/res-xhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xhdpi/images/loop00060.png b/res-xhdpi/images/loop00060.png
index 7f728fc..b7487f3 100644
--- a/res-xhdpi/images/loop00060.png
+++ b/res-xhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xhdpi/images/loop00061.png b/res-xhdpi/images/loop00061.png
index deba021..638704d 100644
--- a/res-xhdpi/images/loop00061.png
+++ b/res-xhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xhdpi/images/loop00062.png b/res-xhdpi/images/loop00062.png
index e6b6184..3cdaee0 100644
--- a/res-xhdpi/images/loop00062.png
+++ b/res-xhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xhdpi/images/loop00063.png b/res-xhdpi/images/loop00063.png
index 0e590a5..c131227 100644
--- a/res-xhdpi/images/loop00063.png
+++ b/res-xhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xhdpi/images/loop00064.png b/res-xhdpi/images/loop00064.png
index c7b8102..4793c10 100644
--- a/res-xhdpi/images/loop00064.png
+++ b/res-xhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xhdpi/images/loop00065.png b/res-xhdpi/images/loop00065.png
index 2ccad25..2c8fff2 100644
--- a/res-xhdpi/images/loop00065.png
+++ b/res-xhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xhdpi/images/loop00066.png b/res-xhdpi/images/loop00066.png
index c5573b9..607fe0b 100644
--- a/res-xhdpi/images/loop00066.png
+++ b/res-xhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xhdpi/images/loop00067.png b/res-xhdpi/images/loop00067.png
index 005e9a6..3ce1005 100644
--- a/res-xhdpi/images/loop00067.png
+++ b/res-xhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xhdpi/images/loop00068.png b/res-xhdpi/images/loop00068.png
index b8d6a6a..a1a850b 100644
--- a/res-xhdpi/images/loop00068.png
+++ b/res-xhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xhdpi/images/loop00069.png b/res-xhdpi/images/loop00069.png
index 7e3ba30..54b2448 100644
--- a/res-xhdpi/images/loop00069.png
+++ b/res-xhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xhdpi/images/loop00070.png b/res-xhdpi/images/loop00070.png
index b9810b3..b2e8f58 100644
--- a/res-xhdpi/images/loop00070.png
+++ b/res-xhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xhdpi/images/loop00071.png b/res-xhdpi/images/loop00071.png
index 726030c..33c9449 100644
--- a/res-xhdpi/images/loop00071.png
+++ b/res-xhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xhdpi/images/loop00072.png b/res-xhdpi/images/loop00072.png
index 30c1e87..0684e17 100644
--- a/res-xhdpi/images/loop00072.png
+++ b/res-xhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xhdpi/images/loop00073.png b/res-xhdpi/images/loop00073.png
index 207a5ac..6f51a87 100644
--- a/res-xhdpi/images/loop00073.png
+++ b/res-xhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xhdpi/images/loop00074.png b/res-xhdpi/images/loop00074.png
index 4482b0c..73d38fd 100644
--- a/res-xhdpi/images/loop00074.png
+++ b/res-xhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xhdpi/images/loop00075.png b/res-xhdpi/images/loop00075.png
index 72afd08..a613a88 100644
--- a/res-xhdpi/images/loop00075.png
+++ b/res-xhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xhdpi/images/loop00076.png b/res-xhdpi/images/loop00076.png
index 4b66068..562fd75 100644
--- a/res-xhdpi/images/loop00076.png
+++ b/res-xhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xhdpi/images/loop00077.png b/res-xhdpi/images/loop00077.png
index a94989e..c1e4ee6 100644
--- a/res-xhdpi/images/loop00077.png
+++ b/res-xhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xhdpi/images/loop00078.png b/res-xhdpi/images/loop00078.png
index 810e223..3d77c78 100644
--- a/res-xhdpi/images/loop00078.png
+++ b/res-xhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xhdpi/images/loop00079.png b/res-xhdpi/images/loop00079.png
index 8085b25..c311148 100644
--- a/res-xhdpi/images/loop00079.png
+++ b/res-xhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xhdpi/images/loop00080.png b/res-xhdpi/images/loop00080.png
index 4aefa4c..c8e8013 100644
--- a/res-xhdpi/images/loop00080.png
+++ b/res-xhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xhdpi/images/loop00081.png b/res-xhdpi/images/loop00081.png
index c4a79fb..c3efcf6 100644
--- a/res-xhdpi/images/loop00081.png
+++ b/res-xhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xhdpi/images/loop00082.png b/res-xhdpi/images/loop00082.png
index 0fc9caa..30e70e6 100644
--- a/res-xhdpi/images/loop00082.png
+++ b/res-xhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xhdpi/images/loop00083.png b/res-xhdpi/images/loop00083.png
index f5fb15d..c984376 100644
--- a/res-xhdpi/images/loop00083.png
+++ b/res-xhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xhdpi/images/loop00084.png b/res-xhdpi/images/loop00084.png
index ada5a25..d06e97c 100644
--- a/res-xhdpi/images/loop00084.png
+++ b/res-xhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xhdpi/images/loop00085.png b/res-xhdpi/images/loop00085.png
index f05e8d6..335c58b 100644
--- a/res-xhdpi/images/loop00085.png
+++ b/res-xhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xhdpi/images/loop00086.png b/res-xhdpi/images/loop00086.png
index 28c5dfd..6fab9dc 100644
--- a/res-xhdpi/images/loop00086.png
+++ b/res-xhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xhdpi/images/loop00087.png b/res-xhdpi/images/loop00087.png
index d969905..a4da498 100644
--- a/res-xhdpi/images/loop00087.png
+++ b/res-xhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xhdpi/images/loop00088.png b/res-xhdpi/images/loop00088.png
index 6533002..b6c4fa5 100644
--- a/res-xhdpi/images/loop00088.png
+++ b/res-xhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xhdpi/images/loop00089.png b/res-xhdpi/images/loop00089.png
index 0d5cdea..cbc7c68 100644
--- a/res-xhdpi/images/loop00089.png
+++ b/res-xhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xhdpi/images/loop00090.png b/res-xhdpi/images/loop00090.png
index b438e9e..0b95c09 100644
--- a/res-xhdpi/images/loop00090.png
+++ b/res-xhdpi/images/loop00090.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00000.png b/res-xxhdpi/images/loop00000.png
index 003c2f8..f723d82 100644
--- a/res-xxhdpi/images/loop00000.png
+++ b/res-xxhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00001.png b/res-xxhdpi/images/loop00001.png
index 05de3dd..8cc111c 100644
--- a/res-xxhdpi/images/loop00001.png
+++ b/res-xxhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00002.png b/res-xxhdpi/images/loop00002.png
index 3b02547..3765cb5 100644
--- a/res-xxhdpi/images/loop00002.png
+++ b/res-xxhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00003.png b/res-xxhdpi/images/loop00003.png
index 21f0dcc..d8dcc18 100644
--- a/res-xxhdpi/images/loop00003.png
+++ b/res-xxhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00004.png b/res-xxhdpi/images/loop00004.png
index 6a8b758..ccd65f7 100644
--- a/res-xxhdpi/images/loop00004.png
+++ b/res-xxhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00005.png b/res-xxhdpi/images/loop00005.png
index a179aef..f996c12 100644
--- a/res-xxhdpi/images/loop00005.png
+++ b/res-xxhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00006.png b/res-xxhdpi/images/loop00006.png
index ef9f5e8..f5dcece 100644
--- a/res-xxhdpi/images/loop00006.png
+++ b/res-xxhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00007.png b/res-xxhdpi/images/loop00007.png
index 80a477d..87ae222 100644
--- a/res-xxhdpi/images/loop00007.png
+++ b/res-xxhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00008.png b/res-xxhdpi/images/loop00008.png
index 6c5cec0..b094a2d 100644
--- a/res-xxhdpi/images/loop00008.png
+++ b/res-xxhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00009.png b/res-xxhdpi/images/loop00009.png
index ac5dd30..88cd77b 100644
--- a/res-xxhdpi/images/loop00009.png
+++ b/res-xxhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00010.png b/res-xxhdpi/images/loop00010.png
index 18f10a1..ab3eb3f 100644
--- a/res-xxhdpi/images/loop00010.png
+++ b/res-xxhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00011.png b/res-xxhdpi/images/loop00011.png
index eac89e9..ab1c4f9 100644
--- a/res-xxhdpi/images/loop00011.png
+++ b/res-xxhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00012.png b/res-xxhdpi/images/loop00012.png
index 390f3cf..48235a2 100644
--- a/res-xxhdpi/images/loop00012.png
+++ b/res-xxhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00013.png b/res-xxhdpi/images/loop00013.png
index 18339e9..443227d 100644
--- a/res-xxhdpi/images/loop00013.png
+++ b/res-xxhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00014.png b/res-xxhdpi/images/loop00014.png
index 77b5be4..aa1361d 100644
--- a/res-xxhdpi/images/loop00014.png
+++ b/res-xxhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00015.png b/res-xxhdpi/images/loop00015.png
index 7c16937..6d1c431 100644
--- a/res-xxhdpi/images/loop00015.png
+++ b/res-xxhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00016.png b/res-xxhdpi/images/loop00016.png
index 50ea46e..c8fbb45 100644
--- a/res-xxhdpi/images/loop00016.png
+++ b/res-xxhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00017.png b/res-xxhdpi/images/loop00017.png
index 40bb9db..5f8f439 100644
--- a/res-xxhdpi/images/loop00017.png
+++ b/res-xxhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00018.png b/res-xxhdpi/images/loop00018.png
index 55b4d70..11fdb23 100644
--- a/res-xxhdpi/images/loop00018.png
+++ b/res-xxhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00019.png b/res-xxhdpi/images/loop00019.png
index a443090..fc15867 100644
--- a/res-xxhdpi/images/loop00019.png
+++ b/res-xxhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00020.png b/res-xxhdpi/images/loop00020.png
index 96e77ee..73b535f 100644
--- a/res-xxhdpi/images/loop00020.png
+++ b/res-xxhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00021.png b/res-xxhdpi/images/loop00021.png
index 35260af..b9d42c9 100644
--- a/res-xxhdpi/images/loop00021.png
+++ b/res-xxhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00022.png b/res-xxhdpi/images/loop00022.png
index 1861848..2213621 100644
--- a/res-xxhdpi/images/loop00022.png
+++ b/res-xxhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00023.png b/res-xxhdpi/images/loop00023.png
index 4b2e7da..3b261b4 100644
--- a/res-xxhdpi/images/loop00023.png
+++ b/res-xxhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00024.png b/res-xxhdpi/images/loop00024.png
index 1ffc765..df97506 100644
--- a/res-xxhdpi/images/loop00024.png
+++ b/res-xxhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00025.png b/res-xxhdpi/images/loop00025.png
index 9fb29d4..510704e 100644
--- a/res-xxhdpi/images/loop00025.png
+++ b/res-xxhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00026.png b/res-xxhdpi/images/loop00026.png
index 143def3..b4f0d17 100644
--- a/res-xxhdpi/images/loop00026.png
+++ b/res-xxhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00027.png b/res-xxhdpi/images/loop00027.png
index 623d6be..9a3ba15 100644
--- a/res-xxhdpi/images/loop00027.png
+++ b/res-xxhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00028.png b/res-xxhdpi/images/loop00028.png
index b7b43d2..d95aa72 100644
--- a/res-xxhdpi/images/loop00028.png
+++ b/res-xxhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00029.png b/res-xxhdpi/images/loop00029.png
index c9f183d..fc88bf9 100644
--- a/res-xxhdpi/images/loop00029.png
+++ b/res-xxhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00030.png b/res-xxhdpi/images/loop00030.png
index b85c7e3..d4692b6 100644
--- a/res-xxhdpi/images/loop00030.png
+++ b/res-xxhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00031.png b/res-xxhdpi/images/loop00031.png
index 4d938e2..500e671 100644
--- a/res-xxhdpi/images/loop00031.png
+++ b/res-xxhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00032.png b/res-xxhdpi/images/loop00032.png
index 0a17876..7a08acf 100644
--- a/res-xxhdpi/images/loop00032.png
+++ b/res-xxhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00033.png b/res-xxhdpi/images/loop00033.png
index c8919c3..8f78362 100644
--- a/res-xxhdpi/images/loop00033.png
+++ b/res-xxhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00034.png b/res-xxhdpi/images/loop00034.png
index 1584d5d..4c54233 100644
--- a/res-xxhdpi/images/loop00034.png
+++ b/res-xxhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00035.png b/res-xxhdpi/images/loop00035.png
index 2220cd3..9eef04e 100644
--- a/res-xxhdpi/images/loop00035.png
+++ b/res-xxhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00036.png b/res-xxhdpi/images/loop00036.png
index 97ae548..bdd243c 100644
--- a/res-xxhdpi/images/loop00036.png
+++ b/res-xxhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00037.png b/res-xxhdpi/images/loop00037.png
index 84fca97..153c03b 100644
--- a/res-xxhdpi/images/loop00037.png
+++ b/res-xxhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00038.png b/res-xxhdpi/images/loop00038.png
index bba2181..5133221 100644
--- a/res-xxhdpi/images/loop00038.png
+++ b/res-xxhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00039.png b/res-xxhdpi/images/loop00039.png
index 4659625..3c0b17d 100644
--- a/res-xxhdpi/images/loop00039.png
+++ b/res-xxhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00040.png b/res-xxhdpi/images/loop00040.png
index 6b3092a..fd5736e 100644
--- a/res-xxhdpi/images/loop00040.png
+++ b/res-xxhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00041.png b/res-xxhdpi/images/loop00041.png
index 5b3cd16..d2a7224 100644
--- a/res-xxhdpi/images/loop00041.png
+++ b/res-xxhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00042.png b/res-xxhdpi/images/loop00042.png
index dbb8a7f..806d58f 100644
--- a/res-xxhdpi/images/loop00042.png
+++ b/res-xxhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00043.png b/res-xxhdpi/images/loop00043.png
index 5824542..dc1106f 100644
--- a/res-xxhdpi/images/loop00043.png
+++ b/res-xxhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00044.png b/res-xxhdpi/images/loop00044.png
index d814246..15ecacd 100644
--- a/res-xxhdpi/images/loop00044.png
+++ b/res-xxhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00045.png b/res-xxhdpi/images/loop00045.png
index e6a8d30..23bbe3e 100644
--- a/res-xxhdpi/images/loop00045.png
+++ b/res-xxhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00046.png b/res-xxhdpi/images/loop00046.png
index 2f616bf..a0a2c06 100644
--- a/res-xxhdpi/images/loop00046.png
+++ b/res-xxhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00047.png b/res-xxhdpi/images/loop00047.png
index 39b74d9..cc2b433 100644
--- a/res-xxhdpi/images/loop00047.png
+++ b/res-xxhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00048.png b/res-xxhdpi/images/loop00048.png
index 2a94b8c..a891efb 100644
--- a/res-xxhdpi/images/loop00048.png
+++ b/res-xxhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00049.png b/res-xxhdpi/images/loop00049.png
index 6d86e2e..da850b3 100644
--- a/res-xxhdpi/images/loop00049.png
+++ b/res-xxhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00050.png b/res-xxhdpi/images/loop00050.png
index c6cb344..1e90f69 100644
--- a/res-xxhdpi/images/loop00050.png
+++ b/res-xxhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00051.png b/res-xxhdpi/images/loop00051.png
index dc510fa..909634a 100644
--- a/res-xxhdpi/images/loop00051.png
+++ b/res-xxhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00052.png b/res-xxhdpi/images/loop00052.png
index 9fdd3ad..e944653 100644
--- a/res-xxhdpi/images/loop00052.png
+++ b/res-xxhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00053.png b/res-xxhdpi/images/loop00053.png
index 8fff9cc..81780a4 100644
--- a/res-xxhdpi/images/loop00053.png
+++ b/res-xxhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00054.png b/res-xxhdpi/images/loop00054.png
index 1f9dfaf..a7a326b 100644
--- a/res-xxhdpi/images/loop00054.png
+++ b/res-xxhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00055.png b/res-xxhdpi/images/loop00055.png
index b0f6690..b887b96 100644
--- a/res-xxhdpi/images/loop00055.png
+++ b/res-xxhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00056.png b/res-xxhdpi/images/loop00056.png
index 79144d9..420e8a1 100644
--- a/res-xxhdpi/images/loop00056.png
+++ b/res-xxhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00057.png b/res-xxhdpi/images/loop00057.png
index a451181..e5a64f1 100644
--- a/res-xxhdpi/images/loop00057.png
+++ b/res-xxhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00058.png b/res-xxhdpi/images/loop00058.png
index eb6af3a..3988ee9 100644
--- a/res-xxhdpi/images/loop00058.png
+++ b/res-xxhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00059.png b/res-xxhdpi/images/loop00059.png
index d9a976d..fa08507 100644
--- a/res-xxhdpi/images/loop00059.png
+++ b/res-xxhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00060.png b/res-xxhdpi/images/loop00060.png
index 93ff5d9..b9e22ee 100644
--- a/res-xxhdpi/images/loop00060.png
+++ b/res-xxhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00061.png b/res-xxhdpi/images/loop00061.png
index 13dcd2a..e5b0123 100644
--- a/res-xxhdpi/images/loop00061.png
+++ b/res-xxhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00062.png b/res-xxhdpi/images/loop00062.png
index 1ffc8f8..1651c70 100644
--- a/res-xxhdpi/images/loop00062.png
+++ b/res-xxhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00063.png b/res-xxhdpi/images/loop00063.png
index 6ec7dae..127add2 100644
--- a/res-xxhdpi/images/loop00063.png
+++ b/res-xxhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00064.png b/res-xxhdpi/images/loop00064.png
index 3c5bcc3..2c9a42e 100644
--- a/res-xxhdpi/images/loop00064.png
+++ b/res-xxhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00065.png b/res-xxhdpi/images/loop00065.png
index 541fa88..1f44c87 100644
--- a/res-xxhdpi/images/loop00065.png
+++ b/res-xxhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00066.png b/res-xxhdpi/images/loop00066.png
index e65ca8f..c27ed05 100644
--- a/res-xxhdpi/images/loop00066.png
+++ b/res-xxhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00067.png b/res-xxhdpi/images/loop00067.png
index c93125b..50c18a3 100644
--- a/res-xxhdpi/images/loop00067.png
+++ b/res-xxhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00068.png b/res-xxhdpi/images/loop00068.png
index f7ef8e9..a6c0d3e 100644
--- a/res-xxhdpi/images/loop00068.png
+++ b/res-xxhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00069.png b/res-xxhdpi/images/loop00069.png
index e3a16c5..9c17c30 100644
--- a/res-xxhdpi/images/loop00069.png
+++ b/res-xxhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00070.png b/res-xxhdpi/images/loop00070.png
index 24cfdb1..4c7fd34 100644
--- a/res-xxhdpi/images/loop00070.png
+++ b/res-xxhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00071.png b/res-xxhdpi/images/loop00071.png
index efffad4..6fcb46d 100644
--- a/res-xxhdpi/images/loop00071.png
+++ b/res-xxhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00072.png b/res-xxhdpi/images/loop00072.png
index 63d62f3..a6cb6b7 100644
--- a/res-xxhdpi/images/loop00072.png
+++ b/res-xxhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00073.png b/res-xxhdpi/images/loop00073.png
index de0f410..d558555 100644
--- a/res-xxhdpi/images/loop00073.png
+++ b/res-xxhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00074.png b/res-xxhdpi/images/loop00074.png
index 45c9a74..90a672c 100644
--- a/res-xxhdpi/images/loop00074.png
+++ b/res-xxhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00075.png b/res-xxhdpi/images/loop00075.png
index a268937..5143985 100644
--- a/res-xxhdpi/images/loop00075.png
+++ b/res-xxhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00076.png b/res-xxhdpi/images/loop00076.png
index 9edd577..c99b7cb 100644
--- a/res-xxhdpi/images/loop00076.png
+++ b/res-xxhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00077.png b/res-xxhdpi/images/loop00077.png
index 23a7cc7..e5f1268 100644
--- a/res-xxhdpi/images/loop00077.png
+++ b/res-xxhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00078.png b/res-xxhdpi/images/loop00078.png
index 67dbf2d..da180ce 100644
--- a/res-xxhdpi/images/loop00078.png
+++ b/res-xxhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00079.png b/res-xxhdpi/images/loop00079.png
index 0ef021f..ece8a33 100644
--- a/res-xxhdpi/images/loop00079.png
+++ b/res-xxhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00080.png b/res-xxhdpi/images/loop00080.png
index 0de307b..be89e16 100644
--- a/res-xxhdpi/images/loop00080.png
+++ b/res-xxhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00081.png b/res-xxhdpi/images/loop00081.png
index cc31e92..77e89ea 100644
--- a/res-xxhdpi/images/loop00081.png
+++ b/res-xxhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00082.png b/res-xxhdpi/images/loop00082.png
index 6809fa3..94771b2 100644
--- a/res-xxhdpi/images/loop00082.png
+++ b/res-xxhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00083.png b/res-xxhdpi/images/loop00083.png
index c3e3a58..b76e8f5 100644
--- a/res-xxhdpi/images/loop00083.png
+++ b/res-xxhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00084.png b/res-xxhdpi/images/loop00084.png
index fc0df35..987fc13 100644
--- a/res-xxhdpi/images/loop00084.png
+++ b/res-xxhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00085.png b/res-xxhdpi/images/loop00085.png
index 38baf7e..6bb681f 100644
--- a/res-xxhdpi/images/loop00085.png
+++ b/res-xxhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00086.png b/res-xxhdpi/images/loop00086.png
index c6616eb..d31d18a 100644
--- a/res-xxhdpi/images/loop00086.png
+++ b/res-xxhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00087.png b/res-xxhdpi/images/loop00087.png
index 2e6b715..797fe7c 100644
--- a/res-xxhdpi/images/loop00087.png
+++ b/res-xxhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00088.png b/res-xxhdpi/images/loop00088.png
index 660d0df..a9764c6 100644
--- a/res-xxhdpi/images/loop00088.png
+++ b/res-xxhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00089.png b/res-xxhdpi/images/loop00089.png
index a6b82c5..b0384ec 100644
--- a/res-xxhdpi/images/loop00089.png
+++ b/res-xxhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xxhdpi/images/loop00090.png b/res-xxhdpi/images/loop00090.png
index 003c2f8..f723d82 100644
--- a/res-xxhdpi/images/loop00090.png
+++ b/res-xxhdpi/images/loop00090.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00000.png b/res-xxxhdpi/images/loop00000.png
index d6640c5..3bca599 100644
--- a/res-xxxhdpi/images/loop00000.png
+++ b/res-xxxhdpi/images/loop00000.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00001.png b/res-xxxhdpi/images/loop00001.png
index e1b82b9..665cd8f 100644
--- a/res-xxxhdpi/images/loop00001.png
+++ b/res-xxxhdpi/images/loop00001.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00002.png b/res-xxxhdpi/images/loop00002.png
index 9b8a381..4f87850 100644
--- a/res-xxxhdpi/images/loop00002.png
+++ b/res-xxxhdpi/images/loop00002.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00003.png b/res-xxxhdpi/images/loop00003.png
index b4d244c..88bdf2d 100644
--- a/res-xxxhdpi/images/loop00003.png
+++ b/res-xxxhdpi/images/loop00003.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00004.png b/res-xxxhdpi/images/loop00004.png
index c923159..db6348e 100644
--- a/res-xxxhdpi/images/loop00004.png
+++ b/res-xxxhdpi/images/loop00004.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00005.png b/res-xxxhdpi/images/loop00005.png
index ed739fa..4ec36c2 100644
--- a/res-xxxhdpi/images/loop00005.png
+++ b/res-xxxhdpi/images/loop00005.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00006.png b/res-xxxhdpi/images/loop00006.png
index 6811692..a00f306 100644
--- a/res-xxxhdpi/images/loop00006.png
+++ b/res-xxxhdpi/images/loop00006.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00007.png b/res-xxxhdpi/images/loop00007.png
index bbeee01..d140655 100644
--- a/res-xxxhdpi/images/loop00007.png
+++ b/res-xxxhdpi/images/loop00007.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00008.png b/res-xxxhdpi/images/loop00008.png
index 2c28032..019843f 100644
--- a/res-xxxhdpi/images/loop00008.png
+++ b/res-xxxhdpi/images/loop00008.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00009.png b/res-xxxhdpi/images/loop00009.png
index 4ea659c..5e9e7bb 100644
--- a/res-xxxhdpi/images/loop00009.png
+++ b/res-xxxhdpi/images/loop00009.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00010.png b/res-xxxhdpi/images/loop00010.png
index 45928bc..52ae668 100644
--- a/res-xxxhdpi/images/loop00010.png
+++ b/res-xxxhdpi/images/loop00010.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00011.png b/res-xxxhdpi/images/loop00011.png
index 8a8f2f7..bd318f6 100644
--- a/res-xxxhdpi/images/loop00011.png
+++ b/res-xxxhdpi/images/loop00011.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00012.png b/res-xxxhdpi/images/loop00012.png
index 1714d1b..114e326 100644
--- a/res-xxxhdpi/images/loop00012.png
+++ b/res-xxxhdpi/images/loop00012.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00013.png b/res-xxxhdpi/images/loop00013.png
index 18ab24f..8890e6d 100644
--- a/res-xxxhdpi/images/loop00013.png
+++ b/res-xxxhdpi/images/loop00013.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00014.png b/res-xxxhdpi/images/loop00014.png
index 5099bc5..72e6b12 100644
--- a/res-xxxhdpi/images/loop00014.png
+++ b/res-xxxhdpi/images/loop00014.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00015.png b/res-xxxhdpi/images/loop00015.png
index b7e6868..cbf0b1b 100644
--- a/res-xxxhdpi/images/loop00015.png
+++ b/res-xxxhdpi/images/loop00015.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00016.png b/res-xxxhdpi/images/loop00016.png
index bc13375..b9f16aa 100644
--- a/res-xxxhdpi/images/loop00016.png
+++ b/res-xxxhdpi/images/loop00016.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00017.png b/res-xxxhdpi/images/loop00017.png
index 8a9bd86..536201a 100644
--- a/res-xxxhdpi/images/loop00017.png
+++ b/res-xxxhdpi/images/loop00017.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00018.png b/res-xxxhdpi/images/loop00018.png
index 2150d63..6ef1b32 100644
--- a/res-xxxhdpi/images/loop00018.png
+++ b/res-xxxhdpi/images/loop00018.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00019.png b/res-xxxhdpi/images/loop00019.png
index ec0cc58..63ffe04 100644
--- a/res-xxxhdpi/images/loop00019.png
+++ b/res-xxxhdpi/images/loop00019.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00020.png b/res-xxxhdpi/images/loop00020.png
index 6596ea2..5e28c00 100644
--- a/res-xxxhdpi/images/loop00020.png
+++ b/res-xxxhdpi/images/loop00020.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00021.png b/res-xxxhdpi/images/loop00021.png
index c874649..0e085f4 100644
--- a/res-xxxhdpi/images/loop00021.png
+++ b/res-xxxhdpi/images/loop00021.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00022.png b/res-xxxhdpi/images/loop00022.png
index d5f834d..9559415 100644
--- a/res-xxxhdpi/images/loop00022.png
+++ b/res-xxxhdpi/images/loop00022.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00023.png b/res-xxxhdpi/images/loop00023.png
index eb8af82..3cc2d5c 100644
--- a/res-xxxhdpi/images/loop00023.png
+++ b/res-xxxhdpi/images/loop00023.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00024.png b/res-xxxhdpi/images/loop00024.png
index 7da5506..fd4addc 100644
--- a/res-xxxhdpi/images/loop00024.png
+++ b/res-xxxhdpi/images/loop00024.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00025.png b/res-xxxhdpi/images/loop00025.png
index 8844149..69fef86 100644
--- a/res-xxxhdpi/images/loop00025.png
+++ b/res-xxxhdpi/images/loop00025.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00026.png b/res-xxxhdpi/images/loop00026.png
index ee36358..a4ff78f 100644
--- a/res-xxxhdpi/images/loop00026.png
+++ b/res-xxxhdpi/images/loop00026.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00027.png b/res-xxxhdpi/images/loop00027.png
index 0299dae..6adc338 100644
--- a/res-xxxhdpi/images/loop00027.png
+++ b/res-xxxhdpi/images/loop00027.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00028.png b/res-xxxhdpi/images/loop00028.png
index a8f5cef..86f2d0e 100644
--- a/res-xxxhdpi/images/loop00028.png
+++ b/res-xxxhdpi/images/loop00028.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00029.png b/res-xxxhdpi/images/loop00029.png
index 6b2ab3f..4ce7d7c 100644
--- a/res-xxxhdpi/images/loop00029.png
+++ b/res-xxxhdpi/images/loop00029.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00030.png b/res-xxxhdpi/images/loop00030.png
index 2d5b48d..0599603 100644
--- a/res-xxxhdpi/images/loop00030.png
+++ b/res-xxxhdpi/images/loop00030.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00031.png b/res-xxxhdpi/images/loop00031.png
index 40c4296..7f96ed5 100644
--- a/res-xxxhdpi/images/loop00031.png
+++ b/res-xxxhdpi/images/loop00031.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00032.png b/res-xxxhdpi/images/loop00032.png
index f130b0a..6f66c5d 100644
--- a/res-xxxhdpi/images/loop00032.png
+++ b/res-xxxhdpi/images/loop00032.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00033.png b/res-xxxhdpi/images/loop00033.png
index 24151ba..4052bb2 100644
--- a/res-xxxhdpi/images/loop00033.png
+++ b/res-xxxhdpi/images/loop00033.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00034.png b/res-xxxhdpi/images/loop00034.png
index f74f895..d751eef 100644
--- a/res-xxxhdpi/images/loop00034.png
+++ b/res-xxxhdpi/images/loop00034.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00035.png b/res-xxxhdpi/images/loop00035.png
index 4a0f805..4d28591 100644
--- a/res-xxxhdpi/images/loop00035.png
+++ b/res-xxxhdpi/images/loop00035.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00036.png b/res-xxxhdpi/images/loop00036.png
index 7465862..8f662e8 100644
--- a/res-xxxhdpi/images/loop00036.png
+++ b/res-xxxhdpi/images/loop00036.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00037.png b/res-xxxhdpi/images/loop00037.png
index 5d10d10..de108e9 100644
--- a/res-xxxhdpi/images/loop00037.png
+++ b/res-xxxhdpi/images/loop00037.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00038.png b/res-xxxhdpi/images/loop00038.png
index 15d5db2..812ff36 100644
--- a/res-xxxhdpi/images/loop00038.png
+++ b/res-xxxhdpi/images/loop00038.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00039.png b/res-xxxhdpi/images/loop00039.png
index b92d49d..e139c43 100644
--- a/res-xxxhdpi/images/loop00039.png
+++ b/res-xxxhdpi/images/loop00039.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00040.png b/res-xxxhdpi/images/loop00040.png
index 5c19c02..0fc29f1 100644
--- a/res-xxxhdpi/images/loop00040.png
+++ b/res-xxxhdpi/images/loop00040.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00041.png b/res-xxxhdpi/images/loop00041.png
index 2c9d406..f9b582d 100644
--- a/res-xxxhdpi/images/loop00041.png
+++ b/res-xxxhdpi/images/loop00041.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00042.png b/res-xxxhdpi/images/loop00042.png
index bb24da5..05678d7 100644
--- a/res-xxxhdpi/images/loop00042.png
+++ b/res-xxxhdpi/images/loop00042.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00043.png b/res-xxxhdpi/images/loop00043.png
index 0a9efd8..eef9bab 100644
--- a/res-xxxhdpi/images/loop00043.png
+++ b/res-xxxhdpi/images/loop00043.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00044.png b/res-xxxhdpi/images/loop00044.png
index 70e1cbc..9a1060e 100644
--- a/res-xxxhdpi/images/loop00044.png
+++ b/res-xxxhdpi/images/loop00044.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00045.png b/res-xxxhdpi/images/loop00045.png
index 0ecb787..41ec4ec 100644
--- a/res-xxxhdpi/images/loop00045.png
+++ b/res-xxxhdpi/images/loop00045.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00046.png b/res-xxxhdpi/images/loop00046.png
index c2c425a..61a87c4 100644
--- a/res-xxxhdpi/images/loop00046.png
+++ b/res-xxxhdpi/images/loop00046.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00047.png b/res-xxxhdpi/images/loop00047.png
index 71812b3..1bb7e86 100644
--- a/res-xxxhdpi/images/loop00047.png
+++ b/res-xxxhdpi/images/loop00047.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00048.png b/res-xxxhdpi/images/loop00048.png
index 6ef44ce..b7d690d 100644
--- a/res-xxxhdpi/images/loop00048.png
+++ b/res-xxxhdpi/images/loop00048.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00049.png b/res-xxxhdpi/images/loop00049.png
index 5c7b1c5..26ad6c2 100644
--- a/res-xxxhdpi/images/loop00049.png
+++ b/res-xxxhdpi/images/loop00049.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00050.png b/res-xxxhdpi/images/loop00050.png
index 10dcf21..555d669 100644
--- a/res-xxxhdpi/images/loop00050.png
+++ b/res-xxxhdpi/images/loop00050.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00051.png b/res-xxxhdpi/images/loop00051.png
index e850b32..f7defeb 100644
--- a/res-xxxhdpi/images/loop00051.png
+++ b/res-xxxhdpi/images/loop00051.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00052.png b/res-xxxhdpi/images/loop00052.png
index 7abf444..915422d 100644
--- a/res-xxxhdpi/images/loop00052.png
+++ b/res-xxxhdpi/images/loop00052.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00053.png b/res-xxxhdpi/images/loop00053.png
index f680849..2dd9b81 100644
--- a/res-xxxhdpi/images/loop00053.png
+++ b/res-xxxhdpi/images/loop00053.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00054.png b/res-xxxhdpi/images/loop00054.png
index 012c14d..e5efd00 100644
--- a/res-xxxhdpi/images/loop00054.png
+++ b/res-xxxhdpi/images/loop00054.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00055.png b/res-xxxhdpi/images/loop00055.png
index ae335db..aa9e379 100644
--- a/res-xxxhdpi/images/loop00055.png
+++ b/res-xxxhdpi/images/loop00055.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00056.png b/res-xxxhdpi/images/loop00056.png
index 8e928ea..d5f355c 100644
--- a/res-xxxhdpi/images/loop00056.png
+++ b/res-xxxhdpi/images/loop00056.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00057.png b/res-xxxhdpi/images/loop00057.png
index c23d4f0..1f06b37 100644
--- a/res-xxxhdpi/images/loop00057.png
+++ b/res-xxxhdpi/images/loop00057.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00058.png b/res-xxxhdpi/images/loop00058.png
index d5144aa..34dc372 100644
--- a/res-xxxhdpi/images/loop00058.png
+++ b/res-xxxhdpi/images/loop00058.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00059.png b/res-xxxhdpi/images/loop00059.png
index f8f3a7c..befec31 100644
--- a/res-xxxhdpi/images/loop00059.png
+++ b/res-xxxhdpi/images/loop00059.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00060.png b/res-xxxhdpi/images/loop00060.png
index 8894a23..0bfb463 100644
--- a/res-xxxhdpi/images/loop00060.png
+++ b/res-xxxhdpi/images/loop00060.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00061.png b/res-xxxhdpi/images/loop00061.png
index 1c33e84..ec622a9 100644
--- a/res-xxxhdpi/images/loop00061.png
+++ b/res-xxxhdpi/images/loop00061.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00062.png b/res-xxxhdpi/images/loop00062.png
index c2242ff..5f0d50c 100644
--- a/res-xxxhdpi/images/loop00062.png
+++ b/res-xxxhdpi/images/loop00062.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00063.png b/res-xxxhdpi/images/loop00063.png
index c357ffa..ce967bd 100644
--- a/res-xxxhdpi/images/loop00063.png
+++ b/res-xxxhdpi/images/loop00063.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00064.png b/res-xxxhdpi/images/loop00064.png
index f946699..4b2f5d7 100644
--- a/res-xxxhdpi/images/loop00064.png
+++ b/res-xxxhdpi/images/loop00064.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00065.png b/res-xxxhdpi/images/loop00065.png
index 52d976b..54ae196 100644
--- a/res-xxxhdpi/images/loop00065.png
+++ b/res-xxxhdpi/images/loop00065.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00066.png b/res-xxxhdpi/images/loop00066.png
index cf37f2f..1c5a4d4 100644
--- a/res-xxxhdpi/images/loop00066.png
+++ b/res-xxxhdpi/images/loop00066.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00067.png b/res-xxxhdpi/images/loop00067.png
index d8a1e78..7a016e7 100644
--- a/res-xxxhdpi/images/loop00067.png
+++ b/res-xxxhdpi/images/loop00067.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00068.png b/res-xxxhdpi/images/loop00068.png
index 8bbaf02..39dbbda 100644
--- a/res-xxxhdpi/images/loop00068.png
+++ b/res-xxxhdpi/images/loop00068.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00069.png b/res-xxxhdpi/images/loop00069.png
index 99d1072..ea2a1e9 100644
--- a/res-xxxhdpi/images/loop00069.png
+++ b/res-xxxhdpi/images/loop00069.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00070.png b/res-xxxhdpi/images/loop00070.png
index bd8979e..303ee3f 100644
--- a/res-xxxhdpi/images/loop00070.png
+++ b/res-xxxhdpi/images/loop00070.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00071.png b/res-xxxhdpi/images/loop00071.png
index e823dcc..74052b1 100644
--- a/res-xxxhdpi/images/loop00071.png
+++ b/res-xxxhdpi/images/loop00071.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00072.png b/res-xxxhdpi/images/loop00072.png
index 475190f..8c2a549 100644
--- a/res-xxxhdpi/images/loop00072.png
+++ b/res-xxxhdpi/images/loop00072.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00073.png b/res-xxxhdpi/images/loop00073.png
index 84c4874..6fdb8e5 100644
--- a/res-xxxhdpi/images/loop00073.png
+++ b/res-xxxhdpi/images/loop00073.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00074.png b/res-xxxhdpi/images/loop00074.png
index e2d90a2..6afb236 100644
--- a/res-xxxhdpi/images/loop00074.png
+++ b/res-xxxhdpi/images/loop00074.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00075.png b/res-xxxhdpi/images/loop00075.png
index ff13dfe..fc9aa77 100644
--- a/res-xxxhdpi/images/loop00075.png
+++ b/res-xxxhdpi/images/loop00075.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00076.png b/res-xxxhdpi/images/loop00076.png
index 01886ae..5a49293 100644
--- a/res-xxxhdpi/images/loop00076.png
+++ b/res-xxxhdpi/images/loop00076.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00077.png b/res-xxxhdpi/images/loop00077.png
index 4bac4ea..4739c13 100644
--- a/res-xxxhdpi/images/loop00077.png
+++ b/res-xxxhdpi/images/loop00077.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00078.png b/res-xxxhdpi/images/loop00078.png
index 6ced1a2..9d79114 100644
--- a/res-xxxhdpi/images/loop00078.png
+++ b/res-xxxhdpi/images/loop00078.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00079.png b/res-xxxhdpi/images/loop00079.png
index f7baed3..59be263 100644
--- a/res-xxxhdpi/images/loop00079.png
+++ b/res-xxxhdpi/images/loop00079.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00080.png b/res-xxxhdpi/images/loop00080.png
index fbb0a13..7bcfbc9 100644
--- a/res-xxxhdpi/images/loop00080.png
+++ b/res-xxxhdpi/images/loop00080.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00081.png b/res-xxxhdpi/images/loop00081.png
index 3fc7a49..3393f83 100644
--- a/res-xxxhdpi/images/loop00081.png
+++ b/res-xxxhdpi/images/loop00081.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00082.png b/res-xxxhdpi/images/loop00082.png
index 3114002..f8d3013 100644
--- a/res-xxxhdpi/images/loop00082.png
+++ b/res-xxxhdpi/images/loop00082.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00083.png b/res-xxxhdpi/images/loop00083.png
index df1b830..733db7b 100644
--- a/res-xxxhdpi/images/loop00083.png
+++ b/res-xxxhdpi/images/loop00083.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00084.png b/res-xxxhdpi/images/loop00084.png
index 11a72f2..d71a10c 100644
--- a/res-xxxhdpi/images/loop00084.png
+++ b/res-xxxhdpi/images/loop00084.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00085.png b/res-xxxhdpi/images/loop00085.png
index ba0a43e..0ea2a19 100644
--- a/res-xxxhdpi/images/loop00085.png
+++ b/res-xxxhdpi/images/loop00085.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00086.png b/res-xxxhdpi/images/loop00086.png
index c4111b2..d2ea05e 100644
--- a/res-xxxhdpi/images/loop00086.png
+++ b/res-xxxhdpi/images/loop00086.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00087.png b/res-xxxhdpi/images/loop00087.png
index 13b83c7..f7814f0 100644
--- a/res-xxxhdpi/images/loop00087.png
+++ b/res-xxxhdpi/images/loop00087.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00088.png b/res-xxxhdpi/images/loop00088.png
index e7d9d6d..b82ab4c 100644
--- a/res-xxxhdpi/images/loop00088.png
+++ b/res-xxxhdpi/images/loop00088.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00089.png b/res-xxxhdpi/images/loop00089.png
index fd1951c..ed354e6 100644
--- a/res-xxxhdpi/images/loop00089.png
+++ b/res-xxxhdpi/images/loop00089.png
Binary files differ
diff --git a/res-xxxhdpi/images/loop00090.png b/res-xxxhdpi/images/loop00090.png
index d6640c5..3bca599 100644
--- a/res-xxxhdpi/images/loop00090.png
+++ b/res-xxxhdpi/images/loop00090.png
Binary files differ
diff --git a/roots.cpp b/roots.cpp
index 184e799..c29771a 100644
--- a/roots.cpp
+++ b/roots.cpp
@@ -39,7 +39,7 @@
#include <ext4_utils/wipe.h>
#include <fs_mgr.h>
-#include "mounts.h"
+#include "otautil/mounts.h"
static struct fstab* fstab = nullptr;
@@ -126,7 +126,7 @@
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, "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";
@@ -283,7 +283,7 @@
if (strcmp(v->fs_type, "ext4") == 0) {
static constexpr int kBlockSize = 4096;
std::vector<std::string> mke2fs_args = {
- "/sbin/mke2fs_static", "-F", "-t", "ext4", "-b", std::to_string(kBlockSize),
+ "/system/bin/mke2fs", "-F", "-t", "ext4", "-b", std::to_string(kBlockSize),
};
int raid_stride = v->logical_blk_size / kBlockSize;
@@ -305,13 +305,7 @@
int result = exec_cmd(mke2fs_args);
if (result == 0 && directory != nullptr) {
std::vector<std::string> e2fsdroid_args = {
- "/sbin/e2fsdroid_static",
- "-e",
- "-f",
- directory,
- "-a",
- volume,
- v->blk_device,
+ "/system/bin/e2fsdroid", "-e", "-f", directory, "-a", volume, v->blk_device,
};
result = exec_cmd(e2fsdroid_args);
}
diff --git a/rotate_logs.cpp b/rotate_logs.cpp
deleted file mode 100644
index da00879..0000000
--- a/rotate_logs.cpp
+++ /dev/null
@@ -1,107 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "rotate_logs.h"
-
-#include <stdio.h>
-#include <string.h>
-#include <sys/types.h>
-
-#include <string>
-
-#include <android-base/file.h>
-#include <android-base/logging.h>
-#include <android-base/parseint.h>
-#include <android-base/stringprintf.h>
-#include <private/android_logger.h> /* private pmsg functions */
-
-static const std::string LAST_KMSG_FILTER = "recovery/last_kmsg";
-static const std::string LAST_LOG_FILTER = "recovery/last_log";
-
-ssize_t logbasename(log_id_t /* id */, char /* prio */, const char* filename, const char* /* buf */,
- size_t len, void* arg) {
- bool* do_rotate = static_cast<bool*>(arg);
- if (LAST_KMSG_FILTER.find(filename) != std::string::npos ||
- LAST_LOG_FILTER.find(filename) != std::string::npos) {
- *do_rotate = true;
- }
- return len;
-}
-
-ssize_t logrotate(log_id_t id, char prio, const char* filename, const char* buf, size_t len,
- void* arg) {
- bool* do_rotate = static_cast<bool*>(arg);
- if (!*do_rotate) {
- return __android_log_pmsg_file_write(id, prio, filename, buf, len);
- }
-
- std::string name(filename);
- size_t dot = name.find_last_of('.');
- std::string sub = name.substr(0, dot);
-
- if (LAST_KMSG_FILTER.find(sub) == std::string::npos &&
- LAST_LOG_FILTER.find(sub) == std::string::npos) {
- return __android_log_pmsg_file_write(id, prio, filename, buf, len);
- }
-
- // filename rotation
- if (dot == std::string::npos) {
- name += ".1";
- } else {
- std::string number = name.substr(dot + 1);
- if (!isdigit(number[0])) {
- name += ".1";
- } else {
- size_t i;
- if (!android::base::ParseUint(number, &i)) {
- LOG(ERROR) << "failed to parse uint in " << number;
- return -1;
- }
- name = sub + "." + std::to_string(i + 1);
- }
- }
-
- return __android_log_pmsg_file_write(id, prio, name.c_str(), buf, len);
-}
-
-// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max.
-// Similarly rename last_kmsg -> last_kmsg.1 -> ... -> last_kmsg.$max.
-// Overwrite any existing last_log.$max and last_kmsg.$max.
-void rotate_logs(const char* last_log_file, const char* last_kmsg_file) {
- // Logs should only be rotated once.
- static bool rotated = false;
- if (rotated) {
- return;
- }
- rotated = true;
-
- for (int i = KEEP_LOG_COUNT - 1; i >= 0; --i) {
- std::string old_log = android::base::StringPrintf("%s", last_log_file);
- if (i > 0) {
- old_log += "." + std::to_string(i);
- }
- std::string new_log = android::base::StringPrintf("%s.%d", last_log_file, i + 1);
- // Ignore errors if old_log doesn't exist.
- rename(old_log.c_str(), new_log.c_str());
-
- std::string old_kmsg = android::base::StringPrintf("%s", last_kmsg_file);
- if (i > 0) {
- old_kmsg += "." + std::to_string(i);
- }
- std::string new_kmsg = android::base::StringPrintf("%s.%d", last_kmsg_file, i + 1);
- rename(old_kmsg.c_str(), new_kmsg.c_str());
- }
-}
diff --git a/screen_ui.cpp b/screen_ui.cpp
index c8fb5aa..391dedb 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -19,8 +19,6 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
-#include <linux/input.h>
-#include <pthread.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
@@ -31,8 +29,11 @@
#include <time.h>
#include <unistd.h>
+#include <algorithm>
+#include <chrono>
#include <memory>
#include <string>
+#include <thread>
#include <unordered_map>
#include <vector>
@@ -40,10 +41,10 @@
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
-#include <minui/minui.h>
-#include "common.h"
#include "device.h"
+#include "minui/minui.h"
+#include "otautil/paths.h"
#include "ui.h"
// Return the current time as a double (including fractions of a second).
@@ -53,11 +54,106 @@
return tv.tv_sec + tv.tv_usec / 1000000.0;
}
-ScreenRecoveryUI::ScreenRecoveryUI()
- : kMarginWidth(RECOVERY_UI_MARGIN_WIDTH),
- kMarginHeight(RECOVERY_UI_MARGIN_HEIGHT),
- kAnimationFps(RECOVERY_UI_ANIMATION_FPS),
- kDensity(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
+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),
+ max_display_items_(max_items),
+ max_item_length_(max_length),
+ text_headers_(headers),
+ menu_start_(0),
+ selection_(initial_selection) {
+ CHECK_LE(max_items, static_cast<size_t>(std::numeric_limits<int>::max()));
+
+ // It's fine to have more entries than text_rows_ if scrollable menu is supported.
+ size_t items_count = scrollable_ ? items.size() : std::min(items.size(), max_display_items_);
+ for (size_t i = 0; i < items_count; ++i) {
+ text_items_.emplace_back(items[i].substr(0, max_item_length_));
+ }
+
+ CHECK(!text_items_.empty());
+}
+
+const std::vector<std::string>& Menu::text_headers() const {
+ return text_headers_;
+}
+
+std::string Menu::TextItem(size_t index) const {
+ CHECK_LT(index, text_items_.size());
+
+ return text_items_[index];
+}
+
+size_t Menu::MenuStart() const {
+ return menu_start_;
+}
+
+size_t Menu::MenuEnd() const {
+ return std::min(ItemsCount(), menu_start_ + max_display_items_);
+}
+
+size_t Menu::ItemsCount() const {
+ return text_items_.size();
+}
+
+bool Menu::ItemsOverflow(std::string* cur_selection_str) const {
+ if (!scrollable_ || ItemsCount() <= max_display_items_) {
+ return false;
+ }
+
+ *cur_selection_str =
+ android::base::StringPrintf("Current item: %zu/%zu", selection_ + 1, ItemsCount());
+ return true;
+}
+
+// TODO(xunchang) modify the function parameters to button up & down.
+int Menu::Select(int sel) {
+ CHECK_LE(ItemsCount(), static_cast<size_t>(std::numeric_limits<int>::max()));
+ int count = ItemsCount();
+
+ // Wraps the selection at boundary if the menu is not scrollable.
+ if (!scrollable_) {
+ if (sel < 0) {
+ selection_ = count - 1;
+ } else if (sel >= count) {
+ selection_ = 0;
+ } else {
+ selection_ = sel;
+ }
+
+ return selection_;
+ }
+
+ if (sel < 0) {
+ selection_ = 0;
+ } else if (sel >= count) {
+ selection_ = count - 1;
+ } else {
+ if (static_cast<size_t>(sel) < menu_start_) {
+ menu_start_--;
+ } else if (static_cast<size_t>(sel) >= MenuEnd()) {
+ menu_start_++;
+ }
+ selection_ = sel;
+ }
+
+ return selection_;
+}
+
+ScreenRecoveryUI::ScreenRecoveryUI() : ScreenRecoveryUI(false) {}
+
+constexpr int kDefaultMarginHeight = 0;
+constexpr int kDefaultMarginWidth = 0;
+constexpr int kDefaultAnimationFps = 30;
+
+ScreenRecoveryUI::ScreenRecoveryUI(bool scrollable_menu)
+ : margin_width_(
+ android::base::GetIntProperty("ro.recovery.ui.margin_width", kDefaultMarginWidth)),
+ margin_height_(
+ android::base::GetIntProperty("ro.recovery.ui.margin_height", kDefaultMarginHeight)),
+ animation_fps_(
+ android::base::GetIntProperty("ro.recovery.ui.animation_fps", kDefaultAnimationFps)),
+ density_(static_cast<float>(android::base::GetIntProperty("ro.sf.lcd_density", 160)) / 160.f),
currentIcon(NONE),
progressBarType(EMPTY),
progressScopeStart(0),
@@ -71,10 +167,7 @@
text_row_(0),
show_text(false),
show_text_ever(false),
- menu_headers_(nullptr),
- show_menu(false),
- menu_items(0),
- menu_sel(0),
+ scrollable_menu_(scrollable_menu),
file_viewer_text_(nullptr),
intro_frames(0),
loop_frames(0),
@@ -83,8 +176,16 @@
stage(-1),
max_stage(-1),
locale_(""),
- rtl_locale_(false),
- updateMutex(PTHREAD_MUTEX_INITIALIZER) {}
+ rtl_locale_(false) {}
+
+ScreenRecoveryUI::~ScreenRecoveryUI() {
+ progress_thread_stopped_ = true;
+ if (progress_thread_.joinable()) {
+ progress_thread_.join();
+ }
+ // No-op if gr_init() (via Init()) was not called or had failed.
+ gr_exit();
+}
GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
if (currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) {
@@ -109,7 +210,7 @@
}
int ScreenRecoveryUI::PixelsFromDp(int dp) const {
- return dp * kDensity;
+ return dp * density_;
}
// Here's the intended layout:
@@ -164,7 +265,7 @@
int stage_height = gr_get_height(stageMarkerEmpty);
int stage_width = gr_get_width(stageMarkerEmpty);
int x = (ScreenWidth() - max_stage * gr_get_width(stageMarkerEmpty)) / 2;
- int y = ScreenHeight() - stage_height - kMarginHeight;
+ int 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);
@@ -275,51 +376,55 @@
surfaces.emplace(name, std::unique_ptr<GRSurface, decltype(&free)>(text_image, &free));
}
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
gr_color(0, 0, 0, 255);
gr_clear();
- int text_y = kMarginHeight;
- int text_x = kMarginWidth;
+ int text_y = margin_height_;
+ int text_x = margin_width_;
int line_spacing = gr_sys_font()->char_height; // Put some extra space between images.
// Write the header and descriptive texts.
SetColor(INFO);
std::string header = "Show background text image";
- text_y += DrawTextLine(text_x, text_y, header.c_str(), true);
+ text_y += DrawTextLine(text_x, text_y, header, true);
std::string locale_selection = android::base::StringPrintf(
- "Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel, locales_entries.size());
- const char* instruction[] = { locale_selection.c_str(),
- "Use volume up/down to switch locales and power to exit.",
- nullptr };
+ "Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel + 1, locales_entries.size());
+ // clang-format off
+ std::vector<std::string> instruction = {
+ locale_selection,
+ "Use volume up/down to switch locales and power to exit."
+ };
+ // clang-format on
text_y += DrawWrappedTextLines(text_x, text_y, instruction);
// Iterate through the text images and display them in order for the current locale.
for (const auto& p : surfaces) {
text_y += line_spacing;
SetColor(LOG);
- text_y += DrawTextLine(text_x, text_y, p.first.c_str(), false);
+ text_y += DrawTextLine(text_x, text_y, p.first, false);
gr_color(255, 255, 255, 255);
gr_texticon(text_x, text_y, p.second.get());
text_y += gr_get_height(p.second.get());
}
// Update the whole screen.
gr_flip();
- pthread_mutex_unlock(&updateMutex);
}
-void ScreenRecoveryUI::CheckBackgroundTextImages(const std::string& saved_locale) {
+void ScreenRecoveryUI::CheckBackgroundTextImages() {
// Load a list of locales embedded in one of the resource files.
std::vector<std::string> locales_entries = get_locales_in_png("installing_text");
if (locales_entries.empty()) {
Print("Failed to load locales from the resource files\n");
return;
}
+ std::string saved_locale = locale_;
size_t selected = 0;
SelectAndShowBackgroundText(locales_entries, selected);
FlushKeys();
while (true) {
int key = WaitKey();
+ if (key == static_cast<int>(KeyError::INTERRUPTED)) break;
if (key == KEY_POWER || key == KEY_ENTER) {
break;
} else if (key == KEY_UP || key == KEY_VOLUMEUP) {
@@ -364,57 +469,51 @@
gr_texticon(x, y, surface);
}
-int ScreenRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
- gr_text(gr_sys_font(), x, y, line, bold);
+int ScreenRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
+ gr_text(gr_sys_font(), x, y, line.c_str(), bold);
return char_height_ + 4;
}
-int ScreenRecoveryUI::DrawTextLines(int x, int y, const char* const* lines) const {
+int ScreenRecoveryUI::DrawTextLines(int x, int y, const std::vector<std::string>& lines) const {
int offset = 0;
- for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
- offset += DrawTextLine(x, y + offset, lines[i], false);
+ for (const auto& line : lines) {
+ offset += DrawTextLine(x, y + offset, line, false);
}
return offset;
}
-int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const char* const* lines) const {
+int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y,
+ const std::vector<std::string>& lines) const {
+ // Keep symmetrical margins based on the given offset (i.e. x).
+ size_t text_cols = (ScreenWidth() - x * 2) / char_width_;
int offset = 0;
- for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
- // The line will be wrapped if it exceeds text_cols_.
- std::string line(lines[i]);
+ for (const auto& line : lines) {
size_t next_start = 0;
while (next_start < line.size()) {
- std::string sub = line.substr(next_start, text_cols_ + 1);
- if (sub.size() <= text_cols_) {
+ std::string sub = line.substr(next_start, text_cols + 1);
+ if (sub.size() <= text_cols) {
next_start += sub.size();
} else {
- // Line too long and must be wrapped to text_cols_ columns.
+ // Line too long and must be wrapped to text_cols columns.
size_t last_space = sub.find_last_of(" \t\n");
if (last_space == std::string::npos) {
- // No space found, just draw as much as we can
- sub.resize(text_cols_);
- next_start += text_cols_;
+ // No space found, just draw as much as we can.
+ sub.resize(text_cols);
+ next_start += text_cols;
} else {
sub.resize(last_space);
next_start += last_space + 1;
}
}
- offset += DrawTextLine(x, y + offset, sub.c_str(), false);
+ offset += DrawTextLine(x, y + offset, sub, false);
}
}
return offset;
}
-static const char* REGULAR_HELP[] = {
- "Use volume up/down and power.",
- NULL
-};
-
-static const char* LONG_PRESS_HELP[] = {
- "Any button cycles highlight.",
- "Long-press activates.",
- NULL
-};
+void ScreenRecoveryUI::SetTitle(const std::vector<std::string>& lines) {
+ title_lines_ = lines;
+}
// Redraws everything on the screen. Does not flip pages. Should only be called with updateMutex
// locked.
@@ -428,38 +527,71 @@
gr_color(0, 0, 0, 255);
gr_clear();
- int y = kMarginHeight;
- if (show_menu) {
+ // clang-format off
+ static std::vector<std::string> REGULAR_HELP{
+ "Use volume up/down and power.",
+ };
+ static std::vector<std::string> LONG_PRESS_HELP{
+ "Any button cycles highlight.",
+ "Long-press activates.",
+ };
+ // clang-format on
+ draw_menu_and_text_buffer_locked(HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
+}
+
+// Draws the menu and text buffer on the screen. Should only be called with updateMutex locked.
+void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(
+ const std::vector<std::string>& help_message) {
+ int y = margin_height_;
+ if (menu_) {
static constexpr int kMenuIndent = 4;
- int x = kMarginWidth + kMenuIndent;
+ int x = margin_width_ + kMenuIndent;
SetColor(INFO);
- y += DrawTextLine(x, y, "Android Recovery", true);
- std::string recovery_fingerprint =
- android::base::GetProperty("ro.bootimage.build.fingerprint", "");
- for (const auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
- y += DrawTextLine(x, y, chunk.c_str(), false);
+
+ for (size_t i = 0; i < title_lines_.size(); i++) {
+ y += DrawTextLine(x, y, title_lines_[i], i == 0);
}
- y += DrawTextLines(x, y, HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
+ y += DrawTextLines(x, y, help_message);
+
+ // Draw menu header.
SetColor(HEADER);
- // Ignore kMenuIndent, which is not taken into account by text_cols_.
- y += DrawWrappedTextLines(kMarginWidth, y, menu_headers_);
+ 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);
- y += DrawHorizontalRule(y) + 4;
- for (int i = 0; i < menu_items; ++i) {
- if (i == menu_sel) {
+ // 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);
- DrawHighlightBar(0, y - 2, ScreenWidth(), char_height_ + 4);
+
+ int bar_height = char_height_ + 4;
+ DrawHighlightBar(0, y - 2, ScreenWidth(), bar_height);
+
// Bold white text for the selected item.
SetColor(MENU_SEL_FG);
- y += DrawTextLine(x, y, menu_[i].c_str(), true);
- SetColor(MENU);
- } else {
- y += DrawTextLine(x, y, menu_[i].c_str(), false);
+ bold = true;
}
+
+ y += DrawTextLine(x, y, menu_->TextItem(i), bold);
+
+ SetColor(MENU);
}
y += DrawHorizontalRule(y);
}
@@ -469,9 +601,9 @@
SetColor(LOG);
int row = text_row_;
size_t count = 0;
- for (int ty = ScreenHeight() - kMarginHeight - char_height_; ty >= y && count < text_rows_;
+ for (int ty = ScreenHeight() - margin_height_ - char_height_; ty >= y && count < text_rows_;
ty -= char_height_, ++count) {
- DrawTextLine(kMarginWidth, ty, text_[row], false);
+ DrawTextLine(margin_width_, ty, text_[row], false);
--row;
if (row < 0) row = text_rows_ - 1;
}
@@ -496,52 +628,46 @@
gr_flip();
}
-// Keeps the progress bar updated, even when the process is otherwise busy.
-void* ScreenRecoveryUI::ProgressThreadStartRoutine(void* data) {
- reinterpret_cast<ScreenRecoveryUI*>(data)->ProgressThreadLoop();
- return nullptr;
-}
-
void ScreenRecoveryUI::ProgressThreadLoop() {
- double interval = 1.0 / kAnimationFps;
- while (true) {
+ double interval = 1.0 / animation_fps_;
+ while (!progress_thread_stopped_) {
double start = now();
- pthread_mutex_lock(&updateMutex);
-
bool redraw = false;
+ {
+ std::lock_guard<std::mutex> lg(updateMutex);
- // update the installation animation, if active
- // skip this if we have a text overlay (too expensive to update)
- if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) && !show_text) {
- if (!intro_done) {
- if (current_frame == intro_frames - 1) {
- intro_done = true;
- current_frame = 0;
+ // update the installation animation, if active
+ // skip this if we have a text overlay (too expensive to update)
+ if ((currentIcon == INSTALLING_UPDATE || currentIcon == ERASING) && !show_text) {
+ if (!intro_done) {
+ if (current_frame == intro_frames - 1) {
+ intro_done = true;
+ current_frame = 0;
+ } else {
+ ++current_frame;
+ }
} else {
- ++current_frame;
+ current_frame = (current_frame + 1) % loop_frames;
}
- } else {
- current_frame = (current_frame + 1) % loop_frames;
- }
- redraw = true;
- }
-
- // move the progress bar forward on timed intervals, if configured
- int duration = progressScopeDuration;
- if (progressBarType == DETERMINATE && duration > 0) {
- double elapsed = now() - progressScopeTime;
- float p = 1.0 * elapsed / duration;
- if (p > 1.0) p = 1.0;
- if (p > progress) {
- progress = p;
redraw = true;
}
+
+ // move the progress bar forward on timed intervals, if configured
+ int duration = progressScopeDuration;
+ if (progressBarType == DETERMINATE && duration > 0) {
+ double elapsed = now() - progressScopeTime;
+ float p = 1.0 * elapsed / duration;
+ if (p > 1.0) p = 1.0;
+ if (p > progress) {
+ progress = p;
+ redraw = true;
+ }
+ }
+
+ if (redraw) update_progress_locked();
}
- if (redraw) update_progress_locked();
-
- pthread_mutex_unlock(&updateMutex);
double end = now();
// minimum of 20ms delay between frames
double delay = interval - (end - start);
@@ -584,19 +710,23 @@
}
bool ScreenRecoveryUI::InitTextParams() {
- if (gr_init() < 0) {
+ // gr_init() would return successfully on font initialization failure.
+ if (gr_sys_font() == nullptr) {
return false;
}
-
gr_font_size(gr_sys_font(), &char_width_, &char_height_);
- text_rows_ = (ScreenHeight() - kMarginHeight * 2) / char_height_;
- text_cols_ = (ScreenWidth() - kMarginWidth * 2) / char_width_;
+ text_rows_ = (ScreenHeight() - margin_height_ * 2) / char_height_;
+ text_cols_ = (ScreenWidth() - margin_width_ * 2) / char_width_;
return true;
}
bool ScreenRecoveryUI::Init(const std::string& locale) {
RecoveryUI::Init(locale);
+ if (gr_init() == -1) {
+ return false;
+ }
+
if (!InitTextParams()) {
return false;
}
@@ -632,13 +762,19 @@
LoadAnimation();
- pthread_create(&progress_thread_, nullptr, ProgressThreadStartRoutine, this);
+ // Keep the progress bar updated, even when the process is otherwise busy.
+ progress_thread_ = std::thread(&ScreenRecoveryUI::ProgressThreadLoop, this);
return true;
}
+std::string ScreenRecoveryUI::GetLocale() const {
+ return locale_;
+}
+
void ScreenRecoveryUI::LoadAnimation() {
- std::unique_ptr<DIR, decltype(&closedir)> dir(opendir("/res/images"), closedir);
+ std::unique_ptr<DIR, decltype(&closedir)> dir(opendir(Paths::Get().resource_dir().c_str()),
+ closedir);
dirent* de;
std::vector<std::string> intro_frame_names;
std::vector<std::string> loop_frame_names;
@@ -675,16 +811,14 @@
}
void ScreenRecoveryUI::SetBackground(Icon icon) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
currentIcon = icon;
update_screen_locked();
-
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::SetProgressType(ProgressType type) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
if (progressBarType != type) {
progressBarType = type;
}
@@ -692,11 +826,10 @@
progressScopeSize = 0;
progress = 0;
update_progress_locked();
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::ShowProgress(float portion, float seconds) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
progressBarType = DETERMINATE;
progressScopeStart += progressScopeSize;
progressScopeSize = portion;
@@ -704,11 +837,10 @@
progressScopeDuration = seconds;
progress = 0;
update_progress_locked();
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::SetProgress(float fraction) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
if (fraction < 0.0) fraction = 0.0;
if (fraction > 1.0) fraction = 1.0;
if (progressBarType == DETERMINATE && fraction > progress) {
@@ -720,14 +852,12 @@
update_progress_locked();
}
}
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::SetStage(int current, int max) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
stage = current;
max_stage = max;
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::PrintV(const char* fmt, bool copy_to_stdout, va_list ap) {
@@ -738,7 +868,7 @@
fputs(str.c_str(), stdout);
}
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
if (text_rows_ > 0 && text_cols_ > 0) {
for (const char* ptr = str.c_str(); *ptr != '\0'; ++ptr) {
if (*ptr == '\n' || text_col_ >= text_cols_) {
@@ -751,7 +881,6 @@
text_[text_row_][text_col_] = '\0';
update_screen_locked();
}
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::Print(const char* fmt, ...) {
@@ -769,23 +898,21 @@
}
void ScreenRecoveryUI::PutChar(char ch) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
if (ch != '\n') text_[text_row_][text_col_++] = ch;
if (ch == '\n' || text_col_ >= text_cols_) {
text_col_ = 0;
++text_row_;
}
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::ClearText() {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
text_col_ = 0;
text_row_ = 0;
for (size_t i = 0; i < text_rows_; ++i) {
memset(text_[i], 0, text_cols_ + 1);
}
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::ShowFile(FILE* fp) {
@@ -806,6 +933,7 @@
while (show_prompt) {
show_prompt = false;
int key = WaitKey();
+ if (key == static_cast<int>(KeyError::INTERRUPTED)) return;
if (key == KEY_POWER || key == KEY_ENTER) {
return;
} else if (key == KEY_UP || key == KEY_VOLUMEUP) {
@@ -838,10 +966,10 @@
}
}
-void ScreenRecoveryUI::ShowFile(const char* filename) {
- FILE* fp = fopen_path(filename, "re");
- if (fp == nullptr) {
- Print(" Unable to open %s: %s\n", filename, strerror(errno));
+void ScreenRecoveryUI::ShowFile(const std::string& filename) {
+ std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(filename.c_str(), "re"), fclose);
+ if (!fp) {
+ Print(" Unable to open %s: %s\n", filename.c_str(), strerror(errno));
return;
}
@@ -853,83 +981,121 @@
text_ = file_viewer_text_;
ClearText();
- ShowFile(fp);
- fclose(fp);
+ ShowFile(fp.get());
text_ = old_text;
text_col_ = old_text_col;
text_row_ = old_text_row;
}
-void ScreenRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
- int initial_selection) {
- pthread_mutex_lock(&updateMutex);
- if (text_rows_ > 0 && text_cols_ > 0) {
- menu_headers_ = headers;
- menu_.clear();
- for (size_t i = 0; i < text_rows_ && items[i] != nullptr; ++i) {
- menu_.emplace_back(std::string(items[i], strnlen(items[i], text_cols_ - 1)));
- }
- menu_items = static_cast<int>(menu_.size());
- show_menu = true;
- menu_sel = initial_selection;
+void ScreenRecoveryUI::StartMenu(const std::vector<std::string>& headers,
+ const std::vector<std::string>& items, size_t initial_selection) {
+ std::lock_guard<std::mutex> lg(updateMutex);
+ if (text_rows_ > 0 && text_cols_ > 1) {
+ menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
+ initial_selection);
update_screen_locked();
}
- pthread_mutex_unlock(&updateMutex);
}
int ScreenRecoveryUI::SelectMenu(int sel) {
- pthread_mutex_lock(&updateMutex);
- if (show_menu) {
- int old_sel = menu_sel;
- menu_sel = sel;
+ std::lock_guard<std::mutex> lg(updateMutex);
+ if (menu_) {
+ int old_sel = menu_->selection();
+ sel = menu_->Select(sel);
- // Wrap at top and bottom.
- if (menu_sel < 0) menu_sel = menu_items - 1;
- if (menu_sel >= menu_items) menu_sel = 0;
-
- sel = menu_sel;
- if (menu_sel != old_sel) update_screen_locked();
+ if (sel != old_sel) {
+ update_screen_locked();
+ }
}
- pthread_mutex_unlock(&updateMutex);
return sel;
}
void ScreenRecoveryUI::EndMenu() {
- pthread_mutex_lock(&updateMutex);
- if (show_menu && text_rows_ > 0 && text_cols_ > 0) {
- show_menu = false;
+ std::lock_guard<std::mutex> lg(updateMutex);
+ if (menu_) {
+ menu_.reset();
update_screen_locked();
}
- pthread_mutex_unlock(&updateMutex);
+}
+
+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) {
+ // Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
+ FlushKeys();
+
+ // If there is a key interrupt in progress, return KeyError::INTERRUPTED without starting the
+ // menu.
+ if (IsKeyInterrupted()) return static_cast<size_t>(KeyError::INTERRUPTED);
+
+ StartMenu(headers, items, initial_selection);
+
+ int selected = initial_selection;
+ int chosen_item = -1;
+ while (chosen_item < 0) {
+ int key = WaitKey();
+ if (key == static_cast<int>(KeyError::INTERRUPTED)) { // WaitKey() was interrupted.
+ return static_cast<size_t>(KeyError::INTERRUPTED);
+ }
+ if (key == static_cast<int>(KeyError::TIMED_OUT)) { // WaitKey() timed out.
+ if (WasTextEverVisible()) {
+ continue;
+ } else {
+ LOG(INFO) << "Timed out waiting for key input; rebooting.";
+ EndMenu();
+ return static_cast<size_t>(KeyError::TIMED_OUT);
+ }
+ }
+
+ bool visible = IsTextVisible();
+ int action = key_handler(key, visible);
+ if (action < 0) {
+ switch (action) {
+ case Device::kHighlightUp:
+ selected = SelectMenu(--selected);
+ break;
+ case Device::kHighlightDown:
+ selected = SelectMenu(++selected);
+ break;
+ case Device::kInvokeItem:
+ chosen_item = selected;
+ break;
+ case Device::kNoAction:
+ break;
+ }
+ } else if (!menu_only) {
+ chosen_item = action;
+ }
+ }
+
+ EndMenu();
+ return chosen_item;
}
bool ScreenRecoveryUI::IsTextVisible() {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
int visible = show_text;
- pthread_mutex_unlock(&updateMutex);
return visible;
}
bool ScreenRecoveryUI::WasTextEverVisible() {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
int ever_visible = show_text_ever;
- pthread_mutex_unlock(&updateMutex);
return ever_visible;
}
void ScreenRecoveryUI::ShowText(bool visible) {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
show_text = visible;
if (show_text) show_text_ever = true;
update_screen_locked();
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::Redraw() {
- pthread_mutex_lock(&updateMutex);
+ std::lock_guard<std::mutex> lg(updateMutex);
update_screen_locked();
- pthread_mutex_unlock(&updateMutex);
}
void ScreenRecoveryUI::KeyLongPress(int) {
@@ -943,9 +1109,9 @@
rtl_locale_ = false;
if (!new_locale.empty()) {
- size_t underscore = new_locale.find('_');
- // lang has the language prefix prior to '_', or full string if '_' doesn't exist.
- std::string lang = new_locale.substr(0, underscore);
+ size_t separator = new_locale.find('-');
+ // lang has the language prefix prior to the separator, or full string if none exists.
+ std::string lang = new_locale.substr(0, separator);
// A bit cheesy: keep an explicit list of supported RTL languages.
if (lang == "ar" || // Arabic
diff --git a/screen_ui.h b/screen_ui.h
index f05761c..f08f4f4 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -17,10 +17,13 @@
#ifndef RECOVERY_SCREEN_UI_H
#define RECOVERY_SCREEN_UI_H
-#include <pthread.h>
#include <stdio.h>
+#include <atomic>
+#include <functional>
+#include <memory>
#include <string>
+#include <thread>
#include <vector>
#include "ui.h"
@@ -28,6 +31,71 @@
// From minui/minui.h.
struct GRSurface;
+// This class maintains the menu selection and display of the screen ui.
+class 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);
+
+ bool scrollable() const {
+ return scrollable_;
+ }
+
+ size_t selection() const {
+ return selection_;
+ }
+
+ // Returns count of menu items.
+ size_t ItemsCount() const;
+
+ // Returns the index of the first menu item.
+ size_t MenuStart() const;
+
+ // Returns the index of the last menu item + 1.
+ size_t MenuEnd() const;
+
+ // Menu example:
+ // info: Android Recovery
+ // ....
+ // help messages: Swipe up/down to move
+ // Swipe left/right to select
+ // empty line (horizontal rule):
+ // menu headers: Select file to view
+ // menu items: /cache/recovery/last_log
+ // /cache/recovery/last_log.1
+ // /cache/recovery/last_log.2
+ // ...
+ const std::vector<std::string>& text_headers() const;
+ std::string TextItem(size_t index) const;
+
+ // Checks if the menu items fit vertically on the screen. Returns true and set the
+ // |cur_selection_str| if the items exceed the screen limit.
+ bool ItemsOverflow(std::string* cur_selection_str) const;
+
+ // 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_;
+ // The max number of menu items to fit vertically on a screen.
+ const size_t max_display_items_;
+ // The length of each item to fit horizontally on a screen.
+ const size_t max_item_length_;
+ // The menu headers.
+ std::vector<std::string> text_headers_;
+ // The actual menu items trimmed to fit the given properties.
+ std::vector<std::string> text_items_;
+ // The first item to display on the screen.
+ size_t menu_start_;
+ // Current menu selection.
+ size_t selection_;
+};
+
// Implementation of RecoveryUI appropriate for devices with a screen
// (shows an icon + a progress bar, text logging, menu, etc.)
class ScreenRecoveryUI : public RecoveryUI {
@@ -44,8 +112,11 @@
};
ScreenRecoveryUI();
+ explicit ScreenRecoveryUI(bool scrollable_menu);
+ ~ScreenRecoveryUI() override;
bool Init(const std::string& locale) override;
+ std::string GetLocale() const override;
// overall recovery state ("background image")
void SetBackground(Icon icon) override;
@@ -66,13 +137,13 @@
// printing messages
void Print(const char* fmt, ...) override __printflike(2, 3);
void PrintOnScreenOnly(const char* fmt, ...) override __printflike(2, 3);
- void ShowFile(const char* filename) override;
+ void ShowFile(const std::string& filename) override;
// menu display
- void StartMenu(const char* const* headers, const char* const* items,
- int initial_selection) override;
- int SelectMenu(int sel) override;
- void EndMenu() override;
+ size_t ShowMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
+ size_t initial_selection, bool menu_only,
+ const std::function<int(int, bool)>& key_handler) override;
+ void SetTitle(const std::vector<std::string>& lines) override;
void KeyLongPress(int) override;
@@ -80,34 +151,46 @@
void SetColor(UIElement e) const;
- // Check the background text image. Use volume up/down button to cycle through the locales
- // embedded in the png file, and power button to go back to recovery main menu.
- void CheckBackgroundTextImages(const std::string& saved_locale);
+ // 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();
protected:
// The margin that we don't want to use for showing texts (e.g. round screen, or screen with
// rounded corners).
- const int kMarginWidth;
- const int kMarginHeight;
+ const int margin_width_;
+ const int margin_height_;
// Number of frames per sec (default: 30) for both parts of the animation.
- const int kAnimationFps;
+ const int animation_fps_;
// The scale factor from dp to pixels. 1.0 for mdpi, 4.0 for xxxhdpi.
- const float kDensity;
+ const float density_;
virtual bool InitTextParams();
+ // 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);
+
+ // 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();
+ virtual void draw_menu_and_text_buffer_locked(const std::vector<std::string>& help_message);
virtual void update_screen_locked();
virtual void update_progress_locked();
GRSurface* GetCurrentFrame() const;
GRSurface* GetCurrentText() const;
- static void* ProgressThreadStartRoutine(void* data);
void ProgressThreadLoop();
virtual void ShowFile(FILE*);
@@ -134,7 +217,7 @@
// Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
virtual int DrawHorizontalRule(int y) const;
// Draws a line of text. Returns the offset it should be moving along Y-axis.
- virtual int DrawTextLine(int x, int y, const char* line, bool bold) const;
+ 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).
@@ -142,10 +225,11 @@
// Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
// Draws multiple text lines. Returns the offset it should be moving along Y-axis.
- int DrawTextLines(int x, int y, const char* const* lines) const;
- // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
- // Returns the offset it should be moving along Y-axis.
- int DrawWrappedTextLines(int x, int y, const char* const* lines) const;
+ 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;
@@ -184,15 +268,16 @@
bool show_text;
bool show_text_ever; // has show_text ever been true?
- std::vector<std::string> menu_;
- const char* const* menu_headers_;
- bool show_menu;
- int menu_items, menu_sel;
+ std::vector<std::string> title_lines_;
+
+ bool scrollable_menu_;
+ std::unique_ptr<Menu> menu_;
// An alternate text screen, swapped with 'text_' when we're viewing a log file.
char** file_viewer_text_;
- pthread_t progress_thread_;
+ 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;
@@ -210,7 +295,7 @@
std::string locale_;
bool rtl_locale_;
- pthread_mutex_t updateMutex;
+ std::mutex updateMutex;
private:
void SetLocale(const std::string&);
diff --git a/stub_ui.h b/stub_ui.h
index 1f6b29a..a3cf12b 100644
--- a/stub_ui.h
+++ b/stub_ui.h
@@ -17,6 +17,10 @@
#ifndef RECOVERY_STUB_UI_H
#define RECOVERY_STUB_UI_H
+#include <functional>
+#include <string>
+#include <vector>
+
#include "ui.h"
// Stub implementation of RecoveryUI for devices without screen.
@@ -24,6 +28,9 @@
public:
StubRecoveryUI() = default;
+ std::string GetLocale() const override {
+ return "";
+ }
void SetBackground(Icon /* icon */) override {}
void SetSystemUpdateText(bool /* security_update */) override {}
@@ -51,15 +58,17 @@
va_end(ap);
}
void PrintOnScreenOnly(const char* /* fmt */, ...) override {}
- void ShowFile(const char* /* filename */) override {}
+ void ShowFile(const std::string& /* filename */) override {}
// menu display
- void StartMenu(const char* const* /* headers */, const char* const* /* items */,
- int /* initial_selection */) override {}
- int SelectMenu(int sel) override {
- return sel;
+ size_t ShowMenu(const std::vector<std::string>& /* headers */,
+ const std::vector<std::string>& /* items */, size_t initial_selection,
+ bool /* menu_only */,
+ const std::function<int(int, bool)>& /* key_handler */) override {
+ return initial_selection;
}
- void EndMenu() override {}
+
+ void SetTitle(const std::vector<std::string>& /* lines */) override {}
};
#endif // RECOVERY_STUB_UI_H
diff --git a/tests/Android.bp b/tests/Android.bp
new file mode 100644
index 0000000..2cfc325
--- /dev/null
+++ b/tests/Android.bp
@@ -0,0 +1,205 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+ name: "recovery_test_defaults",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ include_dirs: [
+ "bootable/recovery",
+ ],
+
+ shared_libs: [
+ "libbase",
+ "libcrypto",
+ "libcutils",
+ "liblog",
+ "libpng",
+ "libselinux",
+ "libz",
+ "libziparchive",
+ ],
+
+ target: {
+ android: {
+ shared_libs: [
+ "libutils",
+ ],
+ },
+
+ host: {
+ static_libs: [
+ "libutils",
+ ],
+ },
+ },
+}
+
+// libapplypatch, libapplypatch_modes, libimgdiff, libimgpatch
+libapplypatch_static_libs = [
+ "libapplypatch_modes",
+ "libapplypatch",
+ "libedify",
+ "libimgdiff",
+ "libimgpatch",
+ "libotautil",
+ "libbsdiff",
+ "libbspatch",
+ "libdivsufsort",
+ "libdivsufsort64",
+ "libutils",
+ "libbase",
+ "libbrotli",
+ "libbz",
+ "libcrypto",
+ "libz",
+ "libziparchive",
+]
+
+// librecovery_defaults uses many shared libs that we want to avoid using in tests (e.g. we don't
+// have 32-bit android.hardware.health@2.0.so or libbootloader_message.so on marlin).
+librecovery_static_libs = [
+ "librecovery",
+ "librecovery_fastboot",
+ "libminui",
+ "libverifier",
+ "libotautil",
+
+ "libhealthhalutils",
+ "libvintf_recovery",
+ "libvintf",
+
+ "android.hardware.health@2.0",
+ "android.hardware.health@1.0",
+ "libbootloader_message",
+ "libext4_utils",
+ "libfs_mgr",
+ "libfusesideload",
+ "libhidl-gen-utils",
+ "libhidlbase",
+ "libhidltransport",
+ "libhwbinder",
+ "libbinderthreadstate",
+ "libvndksupport",
+ "libtinyxml2",
+]
+
+cc_test {
+ name: "recovery_unit_test",
+ isolated: true,
+
+ defaults: [
+ "recovery_test_defaults",
+ ],
+
+ test_suites: ["device-tests"],
+
+ srcs: [
+ "unit/*.cpp",
+ ],
+
+ static_libs: libapplypatch_static_libs + [
+ "librecovery_ui",
+ "libminui",
+ "libverifier",
+ "libotautil",
+ "libupdater",
+ "libgtest_prod",
+ ],
+
+ data: ["testdata/*"],
+}
+
+cc_test {
+ name: "recovery_manual_test",
+ isolated: true,
+
+ defaults: [
+ "recovery_test_defaults",
+ ],
+
+ test_suites: ["device-tests"],
+
+ srcs: [
+ "manual/recovery_test.cpp",
+ ],
+}
+
+cc_test {
+ name: "recovery_component_test",
+ isolated: true,
+
+ defaults: [
+ "recovery_test_defaults",
+ "libupdater_defaults",
+ ],
+
+ test_suites: ["device-tests"],
+
+ srcs: [
+ "component/*.cpp",
+ ],
+
+ static_libs: libapplypatch_static_libs + librecovery_static_libs + [
+ "libupdater",
+ "libupdate_verifier",
+ "libprotobuf-cpp-lite",
+ ],
+
+ data: [
+ "testdata/*",
+ ":res-testdata",
+ ],
+}
+
+cc_test_host {
+ name: "recovery_host_test",
+ isolated: true,
+
+ defaults: [
+ "recovery_test_defaults",
+ ],
+
+ srcs: [
+ "component/imgdiff_test.cpp",
+ ],
+
+ static_libs: [
+ "libimgdiff",
+ "libimgpatch",
+ "libotautil",
+ "libbsdiff",
+ "libbspatch",
+ "libziparchive",
+ "libutils",
+ "libcrypto",
+ "libbrotli",
+ "libbz",
+ "libdivsufsort64",
+ "libdivsufsort",
+ "libz",
+ ],
+
+ data: ["testdata/*"],
+
+ target: {
+ darwin: {
+ // libimgdiff is not available on the Mac.
+ enabled: false,
+ },
+ },
+}
diff --git a/tests/Android.mk b/tests/Android.mk
deleted file mode 100644
index b3584fe..0000000
--- a/tests/Android.mk
+++ /dev/null
@@ -1,223 +0,0 @@
-#
-# Copyright (C) 2014 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-
-LOCAL_PATH := $(call my-dir)
-
-# Unit tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_unit_test
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_STATIC_LIBRARIES := \
- libverifier \
- libminui \
- libotautil \
- libupdater \
- libziparchive \
- libutils \
- libz \
- libselinux \
- libbase \
- libBionicGtestMain
-
-LOCAL_SRC_FILES := \
- unit/asn1_decoder_test.cpp \
- unit/dirutil_test.cpp \
- unit/locale_test.cpp \
- unit/rangeset_test.cpp \
- unit/sysutil_test.cpp \
- unit/zip_test.cpp \
-
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SHARED_LIBRARIES := liblog
-include $(BUILD_NATIVE_TEST)
-
-# Manual tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_manual_test
-LOCAL_STATIC_LIBRARIES := \
- libminui \
- libbase \
- libBionicGtestMain
-
-LOCAL_SRC_FILES := manual/recovery_test.cpp
-LOCAL_SHARED_LIBRARIES := \
- liblog \
- libpng
-
-resource_files := $(call find-files-in-subdirs, bootable/recovery, \
- "*_text.png", \
- res-mdpi/images \
- res-hdpi/images \
- res-xhdpi/images \
- res-xxhdpi/images \
- res-xxxhdpi/images \
- )
-
-# The resource image files that will go to $OUT/data/nativetest/recovery.
-testimage_out_path := $(TARGET_OUT_DATA)/nativetest/recovery
-GEN := $(addprefix $(testimage_out_path)/, $(resource_files))
-
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testimage_out_path)/% : bootable/recovery/%
- $(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-
-include $(BUILD_NATIVE_TEST)
-
-# Component tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := \
- -Wall \
- -Werror \
- -D_FILE_OFFSET_BITS=64
-
-ifeq ($(AB_OTA_UPDATER),true)
-LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
-endif
-
-ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true)
-LOCAL_CFLAGS += -DPRODUCT_SUPPORTS_VERITY=1
-endif
-
-ifeq ($(BOARD_AVB_ENABLE),true)
-LOCAL_CFLAGS += -DBOARD_AVB_ENABLE=1
-endif
-
-LOCAL_MODULE := recovery_component_test
-LOCAL_COMPATIBILITY_SUITE := device-tests
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SRC_FILES := \
- component/applypatch_test.cpp \
- component/bootloader_message_test.cpp \
- component/edify_test.cpp \
- component/imgdiff_test.cpp \
- component/install_test.cpp \
- component/sideload_test.cpp \
- component/uncrypt_test.cpp \
- component/updater_test.cpp \
- component/update_verifier_test.cpp \
- component/verifier_test.cpp
-
-LOCAL_SHARED_LIBRARIES := \
- libhidlbase
-
-tune2fs_static_libraries := \
- libext2_com_err \
- libext2_blkid \
- libext2_quota \
- libext2_uuid \
- libext2_e2p \
- libext2fs
-
-LOCAL_STATIC_LIBRARIES := \
- libapplypatch_modes \
- libapplypatch \
- libedify \
- libimgdiff \
- libimgpatch \
- libbsdiff \
- libbspatch \
- libfusesideload \
- libotafault \
- librecovery \
- libupdater \
- libbootloader_message \
- libverifier \
- libotautil \
- libmounts \
- libupdate_verifier \
- libdivsufsort \
- libdivsufsort64 \
- libfs_mgr \
- libvintf_recovery \
- libvintf \
- libhidl-gen-utils \
- libtinyxml2 \
- libselinux \
- libext4_utils \
- libsparse \
- libcrypto_utils \
- libcrypto \
- libbz \
- libziparchive \
- liblog \
- libutils \
- libz \
- libbase \
- libtune2fs \
- libfec \
- libfec_rs \
- libsquashfs_utils \
- libcutils \
- libbrotli \
- libBionicGtestMain \
- $(tune2fs_static_libraries)
-
-testdata_files := $(call find-subdir-files, testdata/*)
-
-# The testdata files that will go to $OUT/data/nativetest/recovery.
-testdata_out_path := $(TARGET_OUT_DATA)/nativetest/recovery
-GEN := $(addprefix $(testdata_out_path)/, $(testdata_files))
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testdata_out_path)/% : $(LOCAL_PATH)/%
- $(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-
-# A copy of the testdata to be packed into continuous_native_tests.zip.
-testdata_continuous_zip_prefix := \
- $(call intermediates-dir-for,PACKAGING,recovery_component_test)/DATA
-testdata_continuous_zip_path := $(testdata_continuous_zip_prefix)/nativetest/recovery
-GEN := $(addprefix $(testdata_continuous_zip_path)/, $(testdata_files))
-$(GEN): PRIVATE_PATH := $(LOCAL_PATH)
-$(GEN): PRIVATE_CUSTOM_TOOL = cp $< $@
-$(GEN): $(testdata_continuous_zip_path)/% : $(LOCAL_PATH)/%
- $(transform-generated-source)
-LOCAL_GENERATED_SOURCES += $(GEN)
-LOCAL_PICKUP_FILES := $(testdata_continuous_zip_prefix)
-
-include $(BUILD_NATIVE_TEST)
-
-# Host tests
-include $(CLEAR_VARS)
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_MODULE := recovery_host_test
-LOCAL_MODULE_HOST_OS := linux
-LOCAL_C_INCLUDES := bootable/recovery
-LOCAL_SRC_FILES := \
- component/imgdiff_test.cpp
-LOCAL_STATIC_LIBRARIES := \
- libimgdiff \
- libimgpatch \
- libotautil \
- libbsdiff \
- libbspatch \
- libziparchive \
- libutils \
- libbase \
- libcrypto \
- libbrotli \
- libbz \
- libdivsufsort64 \
- libdivsufsort \
- libz \
- libBionicGtestMain
-LOCAL_SHARED_LIBRARIES := \
- liblog
-include $(BUILD_HOST_NATIVE_TEST)
diff --git a/tests/AndroidTest.xml b/tests/AndroidTest.xml
index 3999aa5..6b86085 100644
--- a/tests/AndroidTest.xml
+++ b/tests/AndroidTest.xml
@@ -16,16 +16,18 @@
<configuration description="Config for recovery_component_test and recovery_unit_test">
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="true" />
- <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test" />
- <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test" />
+ <option name="push" value="recovery_component_test->/data/local/tmp/recovery_component_test/recovery_component_test" />
+ <option name="push" value="testdata->/data/local/tmp/recovery_component_test/testdata" />
+ <option name="push" value="recovery_unit_test->/data/local/tmp/recovery_unit_test/recovery_unit_test" />
+ <option name="push" value="testdata->/data/local/tmp/recovery_unit_test/testdata" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
<test class="com.android.tradefed.testtype.GTest" >
- <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="native-test-device-path" value="/data/local/tmp/recovery_component_test" />
<option name="module-name" value="recovery_component_test" />
</test>
<test class="com.android.tradefed.testtype.GTest" >
- <option name="native-test-device-path" value="/data/local/tmp" />
+ <option name="native-test-device-path" value="/data/local/tmp/recovery_unit_test" />
<option name="module-name" value="recovery_unit_test" />
</test>
</configuration>
diff --git a/tests/common/test_constants.h b/tests/common/test_constants.h
index 514818e..b6c27a7 100644
--- a/tests/common/test_constants.h
+++ b/tests/common/test_constants.h
@@ -17,10 +17,10 @@
#ifndef _OTA_TEST_CONSTANTS_H
#define _OTA_TEST_CONSTANTS_H
-#include <stdlib.h>
-
#include <string>
+#include <android-base/file.h>
+
// Zip entries in ziptest_valid.zip.
static const std::string kATxtContents("abcdefghabcdefgh\n");
static const std::string kBTxtContents("abcdefgh\n");
@@ -32,14 +32,9 @@
// echo -n -e "abcdefgh\n" | sha1sum
static const std::string kBTxtSha1Sum("e414af7161c9554089f4106d6f1797ef14a73666");
-static std::string from_testdata_base(const std::string& fname) {
-#ifdef __ANDROID__
- static std::string data_root = getenv("ANDROID_DATA");
-#else
- static std::string data_root = std::string(getenv("ANDROID_PRODUCT_OUT")) + "/data";
-#endif
-
- return data_root + "/nativetest/recovery/testdata/" + fname;
+[[maybe_unused]] static std::string from_testdata_base(const std::string& fname) {
+ static std::string exec_dir = android::base::GetExecutableDirectory();
+ return exec_dir + "/testdata/" + fname;
}
#endif // _OTA_TEST_CONSTANTS_H
diff --git a/tests/component/applypatch_modes_test.cpp b/tests/component/applypatch_modes_test.cpp
new file mode 100644
index 0000000..ce01f4f
--- /dev/null
+++ b/tests/component/applypatch_modes_test.cpp
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agree to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
+#include <bsdiff/bsdiff.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "applypatch/applypatch_modes.h"
+#include "common/test_constants.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+
+using namespace std::string_literals;
+
+// Loads a given partition and returns a string of form "EMMC:name:size:hash".
+static std::string GetEmmcTargetString(const std::string& filename,
+ const std::string& display_name = "") {
+ std::string data;
+ if (!android::base::ReadFileToString(filename, &data)) {
+ PLOG(ERROR) << "Failed to read " << filename;
+ return {};
+ }
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
+
+ return "EMMC:"s + (display_name.empty() ? filename : display_name) + ":" +
+ std::to_string(data.size()) + ":" + print_sha1(digest);
+}
+
+class ApplyPatchModesTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ source = GetEmmcTargetString(from_testdata_base("boot.img"));
+ ASSERT_FALSE(source.empty());
+
+ std::string recovery_file = from_testdata_base("recovery.img");
+ recovery = GetEmmcTargetString(recovery_file);
+ ASSERT_FALSE(recovery.empty());
+
+ ASSERT_TRUE(android::base::WriteStringToFile("", patched_file_.path));
+ target = GetEmmcTargetString(recovery_file, patched_file_.path);
+ ASSERT_FALSE(target.empty());
+
+ Paths::Get().set_cache_temp_source(cache_source_.path);
+ }
+
+ std::string source;
+ std::string target;
+ std::string recovery;
+
+ private:
+ TemporaryFile cache_source_;
+ TemporaryFile patched_file_;
+};
+
+static int InvokeApplyPatchModes(const std::vector<std::string>& args) {
+ auto args_to_call = StringVectorToNullTerminatedArray(args);
+ return applypatch_modes(args_to_call.size() - 1, args_to_call.data());
+}
+
+static void VerifyPatchedTarget(const std::string& target) {
+ std::vector<std::string> pieces = android::base::Split(target, ":");
+ ASSERT_EQ(4, pieces.size());
+ ASSERT_EQ("EMMC", pieces[0]);
+
+ std::string patched_emmc = GetEmmcTargetString(pieces[1]);
+ ASSERT_FALSE(patched_emmc.empty());
+ ASSERT_EQ(target, patched_emmc);
+}
+
+TEST_F(ApplyPatchModesTest, InvalidArgs) {
+ // At least two args (including the filename).
+ ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch" }));
+
+ // Unrecognized args.
+ ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "-x" }));
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
+ std::vector<std::string> args{
+ "applypatch",
+ "--bonus",
+ from_testdata_base("bonus.file"),
+ "--patch",
+ from_testdata_base("recovery-from-boot.p"),
+ "--target",
+ target,
+ "--source",
+ source,
+ };
+ ASSERT_EQ(0, InvokeApplyPatchModes(args));
+ VerifyPatchedTarget(target);
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithoutBonusFile) {
+ std::vector<std::string> args{
+ "applypatch", "--patch", from_testdata_base("recovery-from-boot-with-bonus.p"),
+ "--target", target, "--source",
+ source,
+ };
+
+ ASSERT_EQ(0, InvokeApplyPatchModes(args));
+ VerifyPatchedTarget(target);
+}
+
+// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
+TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) {
+ // Generate the bsdiff patch of recovery-from-boot.p.
+ std::string src_content;
+ ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("boot.img"), &src_content));
+
+ std::string tgt_content;
+ ASSERT_TRUE(android::base::ReadFileToString(from_testdata_base("recovery.img"), &tgt_content));
+
+ TemporaryFile patch_file;
+ ASSERT_EQ(0,
+ bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(),
+ reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(),
+ patch_file.path, nullptr));
+
+ std::vector<std::string> args{
+ "applypatch", "--patch", patch_file.path, "--target", target, "--source", source,
+ };
+ ASSERT_EQ(0, InvokeApplyPatchModes(args));
+ VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) {
+ // Invalid bonus file.
+ std::vector<std::string> args{
+ "applypatch", "--bonus", "/doesntexist", "--patch", from_testdata_base("recovery-from-boot.p"),
+ "--target", target, "--source", source,
+ };
+ ASSERT_NE(0, InvokeApplyPatchModes(args));
+
+ // With bonus file, but missing args.
+ ASSERT_NE(0,
+ InvokeApplyPatchModes({ "applypatch", "--bonus", from_testdata_base("bonus.file") }));
+}
+
+TEST_F(ApplyPatchModesTest, FlashMode) {
+ std::vector<std::string> args{
+ "applypatch", "--flash", from_testdata_base("recovery.img"), "--target", target,
+ };
+ ASSERT_EQ(0, InvokeApplyPatchModes(args));
+ VerifyPatchedTarget(target);
+}
+
+TEST_F(ApplyPatchModesTest, FlashModeInvalidArgs) {
+ std::vector<std::string> args{
+ "applypatch", "--bonus", from_testdata_base("bonus.file"), "--flash", source,
+ "--target", target,
+ };
+ ASSERT_NE(0, InvokeApplyPatchModes(args));
+}
+
+TEST_F(ApplyPatchModesTest, CheckMode) {
+ ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", recovery }));
+ ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--check", source }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
+ ASSERT_EQ(2, InvokeApplyPatchModes({ "applypatch", "--check" }));
+}
+
+TEST_F(ApplyPatchModesTest, CheckModeNonEmmcTarget) {
+ ASSERT_NE(0, InvokeApplyPatchModes({ "applypatch", "--check", from_testdata_base("boot.img") }));
+}
+
+TEST_F(ApplyPatchModesTest, ShowLicenses) {
+ ASSERT_EQ(0, InvokeApplyPatchModes({ "applypatch", "--license" }));
+}
diff --git a/tests/component/applypatch_test.cpp b/tests/component/applypatch_test.cpp
deleted file mode 100644
index 61e06ad..0000000
--- a/tests/component/applypatch_test.cpp
+++ /dev/null
@@ -1,387 +0,0 @@
-/*
- * Copyright (C) 2016 The Android Open Source Project
- *
- * Licensed under the Apache License, version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agree to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include <fcntl.h>
-#include <gtest/gtest.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/stat.h>
-#include <sys/statvfs.h>
-#include <sys/types.h>
-#include <time.h>
-
-#include <memory>
-#include <string>
-#include <vector>
-
-#include <android-base/file.h>
-#include <android-base/stringprintf.h>
-#include <android-base/test_utils.h>
-#include <bsdiff/bsdiff.h>
-#include <openssl/sha.h>
-
-#include "applypatch/applypatch.h"
-#include "applypatch/applypatch_modes.h"
-#include "common/test_constants.h"
-#include "otautil/cache_location.h"
-#include "otautil/print_sha1.h"
-
-using namespace std::string_literals;
-
-static void sha1sum(const std::string& fname, std::string* sha1, size_t* fsize = nullptr) {
- ASSERT_NE(nullptr, sha1);
-
- std::string data;
- ASSERT_TRUE(android::base::ReadFileToString(fname, &data));
-
- if (fsize != nullptr) {
- *fsize = data.size();
- }
-
- uint8_t digest[SHA_DIGEST_LENGTH];
- SHA1(reinterpret_cast<const uint8_t*>(data.c_str()), data.size(), digest);
- *sha1 = print_sha1(digest);
-}
-
-static void mangle_file(const std::string& fname) {
- std::string content(1024, '\0');
- for (size_t i = 0; i < 1024; i++) {
- content[i] = rand() % 256;
- }
- ASSERT_TRUE(android::base::WriteStringToFile(content, fname));
-}
-
-class ApplyPatchTest : public ::testing::Test {
- public:
- virtual void SetUp() override {
- // set up files
- old_file = from_testdata_base("old.file");
- new_file = from_testdata_base("new.file");
- nonexistent_file = from_testdata_base("nonexistent.file");
-
- // set up SHA constants
- sha1sum(old_file, &old_sha1, &old_size);
- sha1sum(new_file, &new_sha1, &new_size);
- srand(time(nullptr));
- bad_sha1_a = android::base::StringPrintf("%040x", rand());
- bad_sha1_b = android::base::StringPrintf("%040x", rand());
- }
-
- std::string old_file;
- std::string new_file;
- std::string nonexistent_file;
-
- std::string old_sha1;
- std::string new_sha1;
- std::string bad_sha1_a;
- std::string bad_sha1_b;
-
- size_t old_size;
- size_t new_size;
-};
-
-class ApplyPatchCacheTest : public ApplyPatchTest {
- protected:
- void SetUp() override {
- ApplyPatchTest::SetUp();
- CacheLocation::location().set_cache_temp_source(old_file);
- }
-};
-
-class ApplyPatchModesTest : public ::testing::Test {
- protected:
- void SetUp() override {
- CacheLocation::location().set_cache_temp_source(cache_source.path);
- }
-
- TemporaryFile cache_source;
-};
-
-TEST_F(ApplyPatchTest, CheckModeSkip) {
- std::vector<std::string> sha1s;
- ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeSingle) {
- std::vector<std::string> sha1s = { old_sha1 };
- ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeMultiple) {
- std::vector<std::string> sha1s = { bad_sha1_a, old_sha1, bad_sha1_b };
- ASSERT_EQ(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeFailure) {
- std::vector<std::string> sha1s = { bad_sha1_a, bad_sha1_b };
- ASSERT_NE(0, applypatch_check(&old_file[0], sha1s));
-}
-
-TEST_F(ApplyPatchTest, CheckModeEmmcTarget) {
- // EMMC:old_file:size:sha1 should pass the check.
- std::string src_file =
- "EMMC:" + old_file + ":" + std::to_string(old_size) + ":" + old_sha1;
- std::vector<std::string> sha1s;
- ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
- // EMMC:old_file:(size-1):sha1:(size+1):sha1 should fail the check.
- src_file = "EMMC:" + old_file + ":" + std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size + 1) + ":" + old_sha1;
- ASSERT_EQ(1, applypatch_check(src_file.c_str(), sha1s));
-
- // EMMC:old_file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check.
- src_file = "EMMC:" + old_file + ":" +
- std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size) + ":" + old_sha1 + ":" +
- std::to_string(old_size + 1) + ":" + old_sha1;
- ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
- // EMMC:old_file:(size+1):sha1:(size-1):sha1:size:sha1 should pass the check.
- src_file = "EMMC:" + old_file + ":" +
- std::to_string(old_size + 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size) + ":" + old_sha1;
- ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-
- // EMMC:new_file:(size+1):old_sha1:(size-1):old_sha1:size:old_sha1:size:new_sha1
- // should pass the check.
- src_file = "EMMC:" + new_file + ":" +
- std::to_string(old_size + 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size - 1) + ":" + old_sha1 + ":" +
- std::to_string(old_size) + ":" + old_sha1 + ":" +
- std::to_string(new_size) + ":" + new_sha1;
- ASSERT_EQ(0, applypatch_check(src_file.c_str(), sha1s));
-}
-
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceSingle) {
- TemporaryFile temp_file;
- mangle_file(temp_file.path);
- std::vector<std::string> sha1s_single = { old_sha1 };
- ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_single));
- ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_single));
-}
-
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceMultiple) {
- TemporaryFile temp_file;
- mangle_file(temp_file.path);
- std::vector<std::string> sha1s_multiple = { bad_sha1_a, old_sha1, bad_sha1_b };
- ASSERT_EQ(0, applypatch_check(temp_file.path, sha1s_multiple));
- ASSERT_EQ(0, applypatch_check(nonexistent_file.c_str(), sha1s_multiple));
-}
-
-TEST_F(ApplyPatchCacheTest, CheckCacheCorruptedSourceFailure) {
- TemporaryFile temp_file;
- mangle_file(temp_file.path);
- std::vector<std::string> sha1s_failure = { bad_sha1_a, bad_sha1_b };
- ASSERT_NE(0, applypatch_check(temp_file.path, sha1s_failure));
- ASSERT_NE(0, applypatch_check(nonexistent_file.c_str(), sha1s_failure));
-}
-
-TEST_F(ApplyPatchModesTest, InvalidArgs) {
- // At least two args (including the filename).
- ASSERT_EQ(2, applypatch_modes(1, (const char* []){ "applypatch" }));
-
- // Unrecognized args.
- ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-x" }));
-}
-
-TEST_F(ApplyPatchModesTest, PatchModeEmmcTarget) {
- std::string boot_img = from_testdata_base("boot.img");
- size_t boot_img_size;
- std::string boot_img_sha1;
- sha1sum(boot_img, &boot_img_sha1, &boot_img_size);
-
- std::string recovery_img = from_testdata_base("recovery.img");
- size_t size;
- std::string recovery_img_sha1;
- sha1sum(recovery_img, &recovery_img_sha1, &size);
- std::string recovery_img_size = std::to_string(size);
-
- std::string bonus_file = from_testdata_base("bonus.file");
-
- // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
- TemporaryFile tmp1;
- std::string src_file =
- "EMMC:" + boot_img + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1;
- std::string tgt_file = "EMMC:" + std::string(tmp1.path);
- std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p");
- std::vector<const char*> args = {
- "applypatch",
- "-b",
- bonus_file.c_str(),
- src_file.c_str(),
- tgt_file.c_str(),
- recovery_img_sha1.c_str(),
- recovery_img_size.c_str(),
- patch.c_str()
- };
- ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
-
- // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
- TemporaryFile tmp2;
- patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p");
- tgt_file = "EMMC:" + std::string(tmp2.path);
- std::vector<const char*> args2 = {
- "applypatch",
- src_file.c_str(),
- tgt_file.c_str(),
- recovery_img_sha1.c_str(),
- recovery_img_size.c_str(),
- patch.c_str()
- };
- ASSERT_EQ(0, applypatch_modes(args2.size(), args2.data()));
-
- // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size> \
- // <src-sha1-fake>:<patch1> <src-sha1>:<patch2>
- TemporaryFile tmp3;
- tgt_file = "EMMC:" + std::string(tmp3.path);
- std::string bad_sha1_a = android::base::StringPrintf("%040x", rand());
- std::string bad_sha1_b = android::base::StringPrintf("%040x", rand());
- std::string patch1 = bad_sha1_a + ":" + from_testdata_base("recovery-from-boot.p");
- std::string patch2 = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot.p");
- std::string patch3 = bad_sha1_b + ":" + from_testdata_base("recovery-from-boot.p");
- std::vector<const char*> args3 = {
- "applypatch",
- "-b",
- bonus_file.c_str(),
- src_file.c_str(),
- tgt_file.c_str(),
- recovery_img_sha1.c_str(),
- recovery_img_size.c_str(),
- patch1.c_str(),
- patch2.c_str(),
- patch3.c_str()
- };
- ASSERT_EQ(0, applypatch_modes(args3.size(), args3.data()));
-}
-
-// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
-TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithBsdiffPatch) {
- std::string boot_img_file = from_testdata_base("boot.img");
- std::string boot_img_sha1;
- size_t boot_img_size;
- sha1sum(boot_img_file, &boot_img_sha1, &boot_img_size);
-
- std::string recovery_img_file = from_testdata_base("recovery.img");
- std::string recovery_img_sha1;
- size_t recovery_img_size;
- sha1sum(recovery_img_file, &recovery_img_sha1, &recovery_img_size);
-
- // Generate the bsdiff patch of recovery-from-boot.p.
- std::string src_content;
- ASSERT_TRUE(android::base::ReadFileToString(boot_img_file, &src_content));
-
- std::string tgt_content;
- ASSERT_TRUE(android::base::ReadFileToString(recovery_img_file, &tgt_content));
-
- TemporaryFile patch_file;
- ASSERT_EQ(0,
- bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(src_content.data()), src_content.size(),
- reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(),
- patch_file.path, nullptr));
-
- // applypatch <src-file> <tgt-file> <tgt-sha1> <tgt-size> <src-sha1>:<patch>
- std::string src_file_arg =
- "EMMC:" + boot_img_file + ":" + std::to_string(boot_img_size) + ":" + boot_img_sha1;
- TemporaryFile tgt_file;
- std::string tgt_file_arg = "EMMC:"s + tgt_file.path;
- std::string recovery_img_size_arg = std::to_string(recovery_img_size);
- std::string patch_arg = boot_img_sha1 + ":" + patch_file.path;
- std::vector<const char*> args = { "applypatch",
- src_file_arg.c_str(),
- tgt_file_arg.c_str(),
- recovery_img_sha1.c_str(),
- recovery_img_size_arg.c_str(),
- patch_arg.c_str() };
- ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
-
- // Double check the patched recovery image.
- std::string tgt_file_sha1;
- size_t tgt_file_size;
- sha1sum(tgt_file.path, &tgt_file_sha1, &tgt_file_size);
- ASSERT_EQ(recovery_img_size, tgt_file_size);
- ASSERT_EQ(recovery_img_sha1, tgt_file_sha1);
-}
-
-TEST_F(ApplyPatchModesTest, PatchModeInvalidArgs) {
- // Invalid bonus file.
- ASSERT_NE(0, applypatch_modes(3, (const char* []){ "applypatch", "-b", "/doesntexist" }));
-
- std::string bonus_file = from_testdata_base("bonus.file");
- // With bonus file, but missing args.
- ASSERT_EQ(2, applypatch_modes(3, (const char* []){ "applypatch", "-b", bonus_file.c_str() }));
-
- std::string boot_img = from_testdata_base("boot.img");
- size_t boot_img_size;
- std::string boot_img_sha1;
- sha1sum(boot_img, &boot_img_sha1, &boot_img_size);
-
- std::string recovery_img = from_testdata_base("recovery.img");
- size_t size;
- std::string recovery_img_sha1;
- sha1sum(recovery_img, &recovery_img_sha1, &size);
- std::string recovery_img_size = std::to_string(size);
-
- // Bonus file is not supported in flash mode.
- // applypatch -b <bonus-file> <src-file> <tgt-file> <tgt-sha1> <tgt-size>
- TemporaryFile tmp4;
- std::vector<const char*> args4 = {
- "applypatch",
- "-b",
- bonus_file.c_str(),
- boot_img.c_str(),
- tmp4.path,
- recovery_img_sha1.c_str(),
- recovery_img_size.c_str()
- };
- ASSERT_NE(0, applypatch_modes(args4.size(), args4.data()));
-
- // Failed to parse patch args.
- TemporaryFile tmp5;
- std::string bad_arg1 =
- "invalid-sha1:filename" + from_testdata_base("recovery-from-boot-with-bonus.p");
- std::vector<const char*> args5 = {
- "applypatch",
- boot_img.c_str(),
- tmp5.path,
- recovery_img_sha1.c_str(),
- recovery_img_size.c_str(),
- bad_arg1.c_str()
- };
- ASSERT_NE(0, applypatch_modes(args5.size(), args5.data()));
-
- // Target size cannot be zero.
- TemporaryFile tmp6;
- std::string patch = boot_img_sha1 + ":" + from_testdata_base("recovery-from-boot-with-bonus.p");
- std::vector<const char*> args6 = {
- "applypatch",
- boot_img.c_str(),
- tmp6.path,
- recovery_img_sha1.c_str(),
- "0", // target size
- patch.c_str()
- };
- ASSERT_NE(0, applypatch_modes(args6.size(), args6.data()));
-}
-
-TEST_F(ApplyPatchModesTest, CheckModeInvalidArgs) {
- // Insufficient args.
- ASSERT_EQ(2, applypatch_modes(2, (const char* []){ "applypatch", "-c" }));
-}
-
-TEST_F(ApplyPatchModesTest, ShowLicenses) {
- ASSERT_EQ(0, applypatch_modes(2, (const char* []){ "applypatch", "-l" }));
-}
diff --git a/tests/component/edify_test.cpp b/tests/component/edify_test.cpp
index 61a1e6b..8397bd3 100644
--- a/tests/component/edify_test.cpp
+++ b/tests/component/edify_test.cpp
@@ -21,30 +21,29 @@
#include "edify/expr.h"
-static void expect(const char* expr_str, const char* expected) {
- std::unique_ptr<Expr> e;
- int error_count = 0;
- EXPECT_EQ(0, parse_string(expr_str, &e, &error_count));
- EXPECT_EQ(0, error_count);
+static void expect(const std::string& expr_str, const char* expected) {
+ std::unique_ptr<Expr> e;
+ int error_count = 0;
+ EXPECT_EQ(0, ParseString(expr_str, &e, &error_count));
+ EXPECT_EQ(0, error_count);
- State state(expr_str, nullptr);
+ State state(expr_str, nullptr);
- std::string result;
- bool status = Evaluate(&state, e, &result);
+ std::string result;
+ bool status = Evaluate(&state, e, &result);
- if (expected == nullptr) {
- EXPECT_FALSE(status);
- } else {
- EXPECT_STREQ(expected, result.c_str());
- }
-
+ if (expected == nullptr) {
+ EXPECT_FALSE(status);
+ } else {
+ EXPECT_STREQ(expected, result.c_str());
+ }
}
class EdifyTest : public ::testing::Test {
- protected:
- virtual void SetUp() {
- RegisterBuiltins();
- }
+ protected:
+ void SetUp() {
+ RegisterBuiltins();
+ }
};
TEST_F(EdifyTest, parsing) {
@@ -146,25 +145,23 @@
}
TEST_F(EdifyTest, big_string) {
- // big string
- expect(std::string(8192, 's').c_str(), std::string(8192, 's').c_str());
+ expect(std::string(8192, 's'), std::string(8192, 's').c_str());
}
TEST_F(EdifyTest, unknown_function) {
- // unknown function
- const char* script1 = "unknown_function()";
- std::unique_ptr<Expr> expr;
- int error_count = 0;
- EXPECT_EQ(1, parse_string(script1, &expr, &error_count));
- EXPECT_EQ(1, error_count);
+ const char* script1 = "unknown_function()";
+ std::unique_ptr<Expr> expr;
+ int error_count = 0;
+ EXPECT_EQ(1, ParseString(script1, &expr, &error_count));
+ EXPECT_EQ(1, error_count);
- const char* script2 = "abc; unknown_function()";
- error_count = 0;
- EXPECT_EQ(1, parse_string(script2, &expr, &error_count));
- EXPECT_EQ(1, error_count);
+ const char* script2 = "abc; unknown_function()";
+ error_count = 0;
+ EXPECT_EQ(1, ParseString(script2, &expr, &error_count));
+ EXPECT_EQ(1, error_count);
- const char* script3 = "unknown_function1() || yes";
- error_count = 0;
- EXPECT_EQ(1, parse_string(script3, &expr, &error_count));
- EXPECT_EQ(1, error_count);
+ const char* script3 = "unknown_function1() || yes";
+ error_count = 0;
+ EXPECT_EQ(1, ParseString(script3, &expr, &error_count));
+ EXPECT_EQ(1, error_count);
}
diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp
index 6c23def..cb4868a 100644
--- a/tests/component/imgdiff_test.cpp
+++ b/tests/component/imgdiff_test.cpp
@@ -197,12 +197,17 @@
}
TEST(ImgdiffTest, zip_mode_smoke_compressed) {
+ // Generate 1 block of random data.
+ std::string random_data;
+ random_data.reserve(4096);
+ generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
// Construct src and tgt zip files.
TemporaryFile src_file;
FILE* src_file_ptr = fdopen(src_file.release(), "wb");
ZipWriter src_writer(src_file_ptr);
ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
- const std::string src_content("abcdefg");
+ const std::string src_content = random_data;
ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
ASSERT_EQ(0, src_writer.FinishEntry());
ASSERT_EQ(0, src_writer.Finish());
@@ -212,7 +217,7 @@
FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
ZipWriter tgt_writer(tgt_file_ptr);
ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
- const std::string tgt_content("abcdefgxyz");
+ const std::string tgt_content = random_data + "extra contents";
ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
ASSERT_EQ(0, tgt_writer.FinishEntry());
ASSERT_EQ(0, tgt_writer.Finish());
@@ -245,13 +250,57 @@
verify_patched_image(src, patch, tgt);
}
+TEST(ImgdiffTest, zip_mode_empty_target) {
+ TemporaryFile src_file;
+ FILE* src_file_ptr = fdopen(src_file.release(), "wb");
+ ZipWriter src_writer(src_file_ptr);
+ ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+ const std::string src_content = "abcdefg";
+ ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
+ ASSERT_EQ(0, src_writer.FinishEntry());
+ ASSERT_EQ(0, src_writer.Finish());
+ ASSERT_EQ(0, fclose(src_file_ptr));
+
+ // Construct a empty entry in the target zip.
+ TemporaryFile tgt_file;
+ FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
+ ZipWriter tgt_writer(tgt_file_ptr);
+ ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
+ const std::string tgt_content;
+ ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
+ ASSERT_EQ(0, tgt_writer.FinishEntry());
+ ASSERT_EQ(0, tgt_writer.Finish());
+
+ // Compute patch.
+ TemporaryFile patch_file;
+ std::vector<const char*> args = {
+ "imgdiff", "-z", src_file.path, tgt_file.path, patch_file.path,
+ };
+ ASSERT_EQ(0, imgdiff(args.size(), args.data()));
+
+ // Verify.
+ std::string tgt;
+ ASSERT_TRUE(android::base::ReadFileToString(tgt_file.path, &tgt));
+ std::string src;
+ ASSERT_TRUE(android::base::ReadFileToString(src_file.path, &src));
+ std::string patch;
+ ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch));
+
+ verify_patched_image(src, patch, tgt);
+}
+
TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) {
+ // Generate 1 block of random data.
+ std::string random_data;
+ random_data.reserve(4096);
+ generate_n(back_inserter(random_data), 4096, []() { return rand() % 256; });
+
// Construct src and tgt zip files.
TemporaryFile src_file;
FILE* src_file_ptr = fdopen(src_file.release(), "wb");
ZipWriter src_writer(src_file_ptr);
ASSERT_EQ(0, src_writer.StartEntry("file1.txt", ZipWriter::kCompress));
- const std::string src_content("abcdefg");
+ const std::string src_content = random_data;
ASSERT_EQ(0, src_writer.WriteBytes(src_content.data(), src_content.size()));
ASSERT_EQ(0, src_writer.FinishEntry());
ASSERT_EQ(0, src_writer.Finish());
@@ -261,7 +310,7 @@
FILE* tgt_file_ptr = fdopen(tgt_file.release(), "wb");
ZipWriter tgt_writer(tgt_file_ptr);
ASSERT_EQ(0, tgt_writer.StartEntry("file1.txt", ZipWriter::kCompress));
- const std::string tgt_content("abcdefgxyz");
+ const std::string tgt_content = random_data + "abcdefg";
ASSERT_EQ(0, tgt_writer.WriteBytes(tgt_content.data(), tgt_content.size()));
ASSERT_EQ(0, tgt_writer.FinishEntry());
ASSERT_EQ(0, tgt_writer.Finish());
@@ -298,23 +347,19 @@
}
TEST(ImgdiffTest, image_mode_simple) {
- // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd).
- const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g',
- 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
- '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
- '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
- '\x00', '\x00', '\x00' };
- const std::string src(src_data.cbegin(), src_data.cend());
+ std::string gzipped_source_path = from_testdata_base("gzipped_source");
+ std::string gzipped_source;
+ ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+ const std::string src = "abcdefg" + gzipped_source;
TemporaryFile src_file;
ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
- // tgt: "abcdefgxyz" + gzipped "xxyyzz".
- const std::vector<char> tgt_data = {
- 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z', '\x1f', '\x8b',
- '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xa8', '\xac',
- '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00', '\x00', '\x00'
- };
- const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+ std::string gzipped_target_path = from_testdata_base("gzipped_target");
+ std::string gzipped_target;
+ ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+ const std::string tgt = "abcdefgxyz" + gzipped_target;
+
TemporaryFile tgt_file;
ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
@@ -404,23 +449,21 @@
}
TEST(ImgdiffTest, image_mode_merge_chunks) {
- // src: "abcdefgh" + gzipped "xyz" (echo -n "xyz" | gzip -f | hd).
- const std::vector<char> src_data = { 'a', 'b', 'c', 'd', 'e', 'f', 'g',
- 'h', '\x1f', '\x8b', '\x08', '\x00', '\xc4', '\x1e',
- '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8', '\xac',
- '\x02', '\x00', '\x67', '\xba', '\x8e', '\xeb', '\x03',
- '\x00', '\x00', '\x00' };
- const std::string src(src_data.cbegin(), src_data.cend());
+ // src: "abcdefg" + gzipped_source.
+ std::string gzipped_source_path = from_testdata_base("gzipped_source");
+ std::string gzipped_source;
+ ASSERT_TRUE(android::base::ReadFileToString(gzipped_source_path, &gzipped_source));
+
+ const std::string src = "abcdefg" + gzipped_source;
TemporaryFile src_file;
ASSERT_TRUE(android::base::WriteStringToFile(src, src_file.path));
- // tgt: gzipped "xyz" + "abcdefgh".
- const std::vector<char> tgt_data = {
- '\x1f', '\x8b', '\x08', '\x00', '\x62', '\x1f', '\x53', '\x58', '\x00', '\x03', '\xab', '\xa8',
- '\xa8', '\xac', '\xac', '\xaa', '\x02', '\x00', '\x96', '\x30', '\x06', '\xb7', '\x06', '\x00',
- '\x00', '\x00', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'x', 'y', 'z'
- };
- const std::string tgt(tgt_data.cbegin(), tgt_data.cend());
+ // tgt: gzipped_target + "abcdefgxyz".
+ std::string gzipped_target_path = from_testdata_base("gzipped_target");
+ std::string gzipped_target;
+ ASSERT_TRUE(android::base::ReadFileToString(gzipped_target_path, &gzipped_target));
+
+ const std::string tgt = gzipped_target + "abcdefgxyz";
TemporaryFile tgt_file;
ASSERT_TRUE(android::base::WriteStringToFile(tgt, tgt_file.path));
diff --git a/tests/component/install_test.cpp b/tests/component/install_test.cpp
index d19d788..08b4290 100644
--- a/tests/component/install_test.cpp
+++ b/tests/component/install_test.cpp
@@ -33,6 +33,7 @@
#include <ziparchive/zip_writer.h>
#include "install.h"
+#include "otautil/paths.h"
#include "private/install.h"
TEST(InstallTest, verify_package_compatibility_no_entry) {
@@ -199,8 +200,73 @@
CloseArchive(zip);
}
-#ifdef AB_OTA_UPDATER
-static void VerifyAbUpdateBinaryCommand(const std::string& serialno, bool success = true) {
+TEST(InstallTest, SetUpNonAbUpdateCommands) {
+ TemporaryFile temp_file;
+ FILE* zip_file = fdopen(temp_file.release(), "w");
+ ZipWriter writer(zip_file);
+ 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));
+
+ ZipArchiveHandle zip;
+ ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+ int status_fd = 10;
+ std::string package = "/path/to/update.zip";
+ TemporaryDir td;
+ std::string binary_path = std::string(td.path) + "/update_binary";
+ Paths::Get().set_temporary_update_binary(binary_path);
+ std::vector<std::string> cmd;
+ ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+ ASSERT_EQ(4U, cmd.size());
+ ASSERT_EQ(binary_path, cmd[0]);
+ ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION
+ ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+ ASSERT_EQ(package, cmd[3]);
+ struct stat sb;
+ ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+ ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+ // With non-zero retry count. update_binary will be removed automatically.
+ cmd.clear();
+ ASSERT_EQ(0, SetUpNonAbUpdateCommands(package, zip, 2, status_fd, &cmd));
+ ASSERT_EQ(5U, cmd.size());
+ ASSERT_EQ(binary_path, cmd[0]);
+ ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION
+ ASSERT_EQ(std::to_string(status_fd), cmd[2]);
+ ASSERT_EQ(package, cmd[3]);
+ ASSERT_EQ("retry", cmd[4]);
+ sb = {};
+ ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
+ ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
+
+ CloseArchive(zip);
+}
+
+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));
+
+ // Missing update binary.
+ ZipArchiveHandle zip;
+ ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
+ int status_fd = 10;
+ std::string package = "/path/to/update.zip";
+ TemporaryDir td;
+ Paths::Get().set_temporary_update_binary(std::string(td.path) + "/update_binary");
+ std::vector<std::string> cmd;
+ ASSERT_EQ(INSTALL_CORRUPT, SetUpNonAbUpdateCommands(package, zip, 0, status_fd, &cmd));
+ CloseArchive(zip);
+}
+
+static void VerifyAbUpdateCommands(const std::string& serialno, bool success = true) {
TemporaryFile temp_file;
FILE* zip_file = fdopen(temp_file.release(), "w");
ZipWriter writer(zip_file);
@@ -235,73 +301,27 @@
ASSERT_EQ(0, FindEntry(zip, payload_name, &payload_entry));
int status_fd = 10;
std::string package = "/path/to/update.zip";
- std::string binary_path = "/sbin/update_engine_sideload";
std::vector<std::string> cmd;
if (success) {
- ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+ ASSERT_EQ(0, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
ASSERT_EQ(5U, cmd.size());
- ASSERT_EQ(binary_path, cmd[0]);
+ ASSERT_EQ("/system/bin/update_engine_sideload", cmd[0]);
ASSERT_EQ("--payload=file://" + package, cmd[1]);
ASSERT_EQ("--offset=" + std::to_string(payload_entry.offset), cmd[2]);
ASSERT_EQ("--headers=" + properties, cmd[3]);
ASSERT_EQ("--status_fd=" + std::to_string(status_fd), cmd[4]);
} else {
- ASSERT_EQ(INSTALL_ERROR, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+ ASSERT_EQ(INSTALL_ERROR, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
}
CloseArchive(zip);
}
-#endif // AB_OTA_UPDATER
-TEST(InstallTest, update_binary_command_smoke) {
-#ifdef AB_OTA_UPDATER
+TEST(InstallTest, SetUpAbUpdateCommands) {
// Empty serialno will pass the verification.
- VerifyAbUpdateBinaryCommand({});
-#else
- TemporaryFile temp_file;
- FILE* zip_file = fdopen(temp_file.release(), "w");
- ZipWriter writer(zip_file);
- static constexpr const char* UPDATE_BINARY_NAME = "META-INF/com/google/android/update-binary";
- ASSERT_EQ(0, writer.StartEntry(UPDATE_BINARY_NAME, kCompressStored));
- ASSERT_EQ(0, writer.FinishEntry());
- ASSERT_EQ(0, writer.Finish());
- ASSERT_EQ(0, fclose(zip_file));
-
- ZipArchiveHandle zip;
- ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
- int status_fd = 10;
- std::string package = "/path/to/update.zip";
- TemporaryDir td;
- std::string binary_path = std::string(td.path) + "/update_binary";
- std::vector<std::string> cmd;
- ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
- ASSERT_EQ(4U, cmd.size());
- ASSERT_EQ(binary_path, cmd[0]);
- ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION
- ASSERT_EQ(std::to_string(status_fd), cmd[2]);
- ASSERT_EQ(package, cmd[3]);
- struct stat sb;
- ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
- ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-
- // With non-zero retry count. update_binary will be removed automatically.
- cmd.clear();
- ASSERT_EQ(0, update_binary_command(package, zip, binary_path, 2, status_fd, &cmd));
- ASSERT_EQ(5U, cmd.size());
- ASSERT_EQ(binary_path, cmd[0]);
- ASSERT_EQ("3", cmd[1]); // RECOVERY_API_VERSION
- ASSERT_EQ(std::to_string(status_fd), cmd[2]);
- ASSERT_EQ(package, cmd[3]);
- ASSERT_EQ("retry", cmd[4]);
- sb = {};
- ASSERT_EQ(0, stat(binary_path.c_str(), &sb));
- ASSERT_EQ(static_cast<mode_t>(0755), sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO));
-
- CloseArchive(zip);
-#endif // AB_OTA_UPDATER
+ VerifyAbUpdateCommands({});
}
-TEST(InstallTest, update_binary_command_invalid) {
-#ifdef AB_OTA_UPDATER
+TEST(InstallTest, SetUpAbUpdateCommands_MissingPayloadPropertiesTxt) {
TemporaryFile temp_file;
FILE* zip_file = fdopen(temp_file.release(), "w");
ZipWriter writer(zip_file);
@@ -328,60 +348,36 @@
ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
int status_fd = 10;
std::string package = "/path/to/update.zip";
- std::string binary_path = "/sbin/update_engine_sideload";
std::vector<std::string> cmd;
- ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
+ ASSERT_EQ(INSTALL_CORRUPT, SetUpAbUpdateCommands(package, zip, status_fd, &cmd));
CloseArchive(zip);
-#else
- TemporaryFile temp_file;
- FILE* zip_file = fdopen(temp_file.release(), "w");
- ZipWriter writer(zip_file);
- // The archive must have something to be opened correctly.
- ASSERT_EQ(0, writer.StartEntry("dummy_entry", 0));
- ASSERT_EQ(0, writer.FinishEntry());
- ASSERT_EQ(0, writer.Finish());
- ASSERT_EQ(0, fclose(zip_file));
-
- // Missing update binary.
- ZipArchiveHandle zip;
- ASSERT_EQ(0, OpenArchive(temp_file.path, &zip));
- int status_fd = 10;
- std::string package = "/path/to/update.zip";
- TemporaryDir td;
- std::string binary_path = std::string(td.path) + "/update_binary";
- std::vector<std::string> cmd;
- ASSERT_EQ(INSTALL_CORRUPT, update_binary_command(package, zip, binary_path, 0, status_fd, &cmd));
- CloseArchive(zip);
-#endif // AB_OTA_UPDATER
}
-#ifdef AB_OTA_UPDATER
-TEST(InstallTest, update_binary_command_multiple_serialno) {
+TEST(InstallTest, SetUpAbUpdateCommands_MultipleSerialnos) {
std::string serialno = android::base::GetProperty("ro.serialno", "");
ASSERT_NE("", serialno);
// Single matching serialno will pass the verification.
- VerifyAbUpdateBinaryCommand(serialno);
+ VerifyAbUpdateCommands(serialno);
static constexpr char alphabet[] =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
auto generator = []() { return alphabet[rand() % (sizeof(alphabet) - 1)]; };
// Generate 900 random serial numbers.
- std::string random_serial;
+ std::string random_serialno;
for (size_t i = 0; i < 900; i++) {
- generate_n(back_inserter(random_serial), serialno.size(), generator);
- random_serial.append("|");
+ generate_n(back_inserter(random_serialno), serialno.size(), generator);
+ random_serialno.append("|");
}
// Random serialnos should fail the verification.
- VerifyAbUpdateBinaryCommand(random_serial, false);
+ VerifyAbUpdateCommands(random_serialno, false);
- std::string long_serial = random_serial + serialno + "|";
+ std::string long_serialno = random_serialno + serialno + "|";
for (size_t i = 0; i < 99; i++) {
- generate_n(back_inserter(long_serial), serialno.size(), generator);
- long_serial.append("|");
+ generate_n(back_inserter(long_serialno), serialno.size(), generator);
+ long_serialno.append("|");
}
// String with the matching serialno should pass the verification.
- VerifyAbUpdateBinaryCommand(long_serial);
+ VerifyAbUpdateCommands(long_serialno);
}
-#endif // AB_OTA_UPDATER
diff --git a/tests/component/resources_test.cpp b/tests/component/resources_test.cpp
new file mode 100644
index 0000000..54329db
--- /dev/null
+++ b/tests/component/resources_test.cpp
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <dirent.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <png.h>
+
+#include "minui/minui.h"
+#include "private/resources.h"
+
+static const std::string kLocale = "zu";
+
+static const std::vector<std::string> kResourceImagesDirs{
+ "res-mdpi/images/", "res-hdpi/images/", "res-xhdpi/images/",
+ "res-xxhdpi/images/", "res-xxxhdpi/images/",
+};
+
+static int png_filter(const dirent* de) {
+ if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) {
+ return 0;
+ }
+ return 1;
+}
+
+// Finds out all the PNG files to test, which stay under the same dir with the executabl..
+static std::vector<std::string> add_files() {
+ std::vector<std::string> files;
+ for (const std::string& images_dir : kResourceImagesDirs) {
+ static std::string exec_dir = android::base::GetExecutableDirectory();
+ std::string dir_path = exec_dir + "/" + images_dir;
+ dirent** namelist;
+ int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort);
+ if (n == -1) {
+ printf("Failed to scandir %s: %s\n", dir_path.c_str(), strerror(errno));
+ continue;
+ }
+ if (n == 0) {
+ printf("No file is added for test in %s\n", dir_path.c_str());
+ }
+
+ while (n--) {
+ std::string file_path = dir_path + namelist[n]->d_name;
+ files.push_back(file_path);
+ free(namelist[n]);
+ }
+ free(namelist);
+ }
+ return files;
+}
+
+class ResourcesTest : public testing::TestWithParam<std::string> {
+ public:
+ static std::vector<std::string> png_list;
+
+ protected:
+ void SetUp() override {
+ png_ = std::make_unique<PngHandler>(GetParam());
+ ASSERT_TRUE(png_);
+
+ ASSERT_EQ(PNG_COLOR_TYPE_GRAY, png_->color_type()) << "Recovery expects grayscale PNG file.";
+ ASSERT_LT(static_cast<png_uint_32>(5), png_->width());
+ ASSERT_LT(static_cast<png_uint_32>(0), png_->height());
+ ASSERT_EQ(1, png_->channels()) << "Recovery background text images expects 1-channel PNG file.";
+ }
+
+ std::unique_ptr<PngHandler> png_{ nullptr };
+};
+
+// Parses a png file and tests if it's qualified for the background text image under recovery.
+TEST_P(ResourcesTest, ValidateLocale) {
+ std::vector<unsigned char> row(png_->width());
+ for (png_uint_32 y = 0; y < png_->height(); ++y) {
+ png_read_row(png_->png_ptr(), row.data(), nullptr);
+ int w = (row[1] << 8) | row[0];
+ int h = (row[3] << 8) | row[2];
+ int len = row[4];
+ EXPECT_LT(0, w);
+ EXPECT_LT(0, h);
+ EXPECT_LT(0, len) << "Locale string should be non-empty.";
+ EXPECT_NE(0, row[5]) << "Locale string is missing.";
+
+ ASSERT_GT(png_->height(), y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
+ char* loc = reinterpret_cast<char*>(&row[5]);
+ if (matches_locale(loc, kLocale.c_str())) {
+ EXPECT_TRUE(android::base::StartsWith(loc, kLocale));
+ break;
+ }
+ for (int i = 0; i < h; ++i, ++y) {
+ png_read_row(png_->png_ptr(), row.data(), nullptr);
+ }
+ }
+}
+
+std::vector<std::string> ResourcesTest::png_list = add_files();
+
+INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourcesTest,
+ ::testing::ValuesIn(ResourcesTest::png_list.cbegin(),
+ ResourcesTest::png_list.cend()));
diff --git a/tests/component/update_verifier_test.cpp b/tests/component/update_verifier_test.cpp
index 1544bb2..2420c27 100644
--- a/tests/component/update_verifier_test.cpp
+++ b/tests/component/update_verifier_test.cpp
@@ -14,29 +14,89 @@
* limitations under the License.
*/
+#include <update_verifier/update_verifier.h>
+
+#include <functional>
#include <string>
+#include <unordered_map>
+#include <vector>
#include <android-base/file.h>
+#include <android-base/properties.h>
+#include <android-base/strings.h>
#include <android-base/test_utils.h>
+#include <google/protobuf/repeated_field.h>
#include <gtest/gtest.h>
-#include <update_verifier/update_verifier.h>
+
+#include "care_map.pb.h"
+
+using namespace std::string_literals;
class UpdateVerifierTest : public ::testing::Test {
protected:
void SetUp() override {
-#if defined(PRODUCT_SUPPORTS_VERITY) || defined(BOARD_AVB_ENABLE)
- verity_supported = true;
-#else
- verity_supported = false;
-#endif
+ std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
+ verity_supported = android::base::EqualsIgnoreCase(verity_mode, "enforcing");
+
+ care_map_prefix_ = care_map_dir_.path + "/care_map"s;
+ care_map_pb_ = care_map_dir_.path + "/care_map.pb"s;
+ care_map_txt_ = care_map_dir_.path + "/care_map.txt"s;
+ // Overrides the the care_map_prefix.
+ verifier_.set_care_map_prefix(care_map_prefix_);
+
+ property_id_ = "ro.build.fingerprint";
+ fingerprint_ = android::base::GetProperty(property_id_, "");
+ // Overrides the property_reader if we cannot read the given property on the device.
+ if (fingerprint_.empty()) {
+ fingerprint_ = "mock_fingerprint";
+ verifier_.set_property_reader([](const std::string& /* id */) { return "mock_fingerprint"; });
+ }
+ }
+
+ void TearDown() override {
+ unlink(care_map_pb_.c_str());
+ unlink(care_map_txt_.c_str());
+ }
+
+ // Returns a serialized string of the proto3 message according to the given partition info.
+ std::string ConstructProto(
+ std::vector<std::unordered_map<std::string, std::string>>& partitions) {
+ recovery_update_verifier::CareMap result;
+ for (const auto& partition : partitions) {
+ recovery_update_verifier::CareMap::PartitionInfo info;
+ if (partition.find("name") != partition.end()) {
+ info.set_name(partition.at("name"));
+ }
+ if (partition.find("ranges") != partition.end()) {
+ info.set_ranges(partition.at("ranges"));
+ }
+ if (partition.find("id") != partition.end()) {
+ info.set_id(partition.at("id"));
+ }
+ if (partition.find("fingerprint") != partition.end()) {
+ info.set_fingerprint(partition.at("fingerprint"));
+ }
+
+ *result.add_partitions() = info;
+ }
+
+ return result.SerializeAsString();
}
bool verity_supported;
+ UpdateVerifier verifier_;
+
+ TemporaryDir care_map_dir_;
+ std::string care_map_prefix_;
+ std::string care_map_pb_;
+ std::string care_map_txt_;
+
+ std::string property_id_;
+ std::string fingerprint_;
};
TEST_F(UpdateVerifierTest, verify_image_no_care_map) {
- // Non-existing care_map is allowed.
- ASSERT_TRUE(verify_image("/doesntexist"));
+ ASSERT_FALSE(verifier_.ParseCareMap());
}
TEST_F(UpdateVerifierTest, verify_image_smoke) {
@@ -46,26 +106,28 @@
return;
}
- TemporaryFile temp_file;
std::string content = "system\n2,0,1";
- ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
- ASSERT_TRUE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+ ASSERT_TRUE(verifier_.ParseCareMap());
+ ASSERT_TRUE(verifier_.VerifyPartitions());
// Leading and trailing newlines should be accepted.
- ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", temp_file.path));
- ASSERT_TRUE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile("\n" + content + "\n\n", care_map_txt_));
+ ASSERT_TRUE(verifier_.ParseCareMap());
+ ASSERT_TRUE(verifier_.VerifyPartitions());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_empty_care_map) {
+ ASSERT_FALSE(verifier_.ParseCareMap());
}
TEST_F(UpdateVerifierTest, verify_image_wrong_lines) {
// The care map file can have only 2 / 4 / 6 lines.
- TemporaryFile temp_file;
- ASSERT_FALSE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile("line1", care_map_txt_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
- ASSERT_TRUE(android::base::WriteStringToFile("line1", temp_file.path));
- ASSERT_FALSE(verify_image(temp_file.path));
-
- ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", temp_file.path));
- ASSERT_FALSE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile("line1\nline2\nline3", care_map_txt_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
}
TEST_F(UpdateVerifierTest, verify_image_malformed_care_map) {
@@ -75,10 +137,9 @@
return;
}
- TemporaryFile temp_file;
std::string content = "system\n2,1,0";
- ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
- ASSERT_FALSE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
}
TEST_F(UpdateVerifierTest, verify_image_legacy_care_map) {
@@ -88,8 +149,110 @@
return;
}
- TemporaryFile temp_file;
std::string content = "/dev/block/bootdevice/by-name/system\n2,1,0";
- ASSERT_TRUE(android::base::WriteStringToFile(content, temp_file.path));
- ASSERT_TRUE(verify_image(temp_file.path));
+ ASSERT_TRUE(android::base::WriteStringToFile(content, care_map_txt_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_smoke) {
+ // This test relies on dm-verity support.
+ if (!verity_supported) {
+ GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+ return;
+ }
+
+ std::vector<std::unordered_map<std::string, std::string>> partitions = {
+ {
+ { "name", "system" },
+ { "ranges", "2,0,1" },
+ { "id", property_id_ },
+ { "fingerprint", fingerprint_ },
+ },
+ };
+
+ std::string proto = ConstructProto(partitions);
+ ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+ ASSERT_TRUE(verifier_.ParseCareMap());
+ ASSERT_TRUE(verifier_.VerifyPartitions());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_missing_name) {
+ // This test relies on dm-verity support.
+ if (!verity_supported) {
+ GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+ return;
+ }
+
+ std::vector<std::unordered_map<std::string, std::string>> partitions = {
+ {
+ { "ranges", "2,0,1" },
+ { "id", property_id_ },
+ { "fingerprint", fingerprint_ },
+ },
+ };
+
+ std::string proto = ConstructProto(partitions);
+ ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_care_map_bad_ranges) {
+ // This test relies on dm-verity support.
+ if (!verity_supported) {
+ GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+ return;
+ }
+
+ std::vector<std::unordered_map<std::string, std::string>> partitions = {
+ {
+ { "name", "system" },
+ { "ranges", "3,0,1" },
+ { "id", property_id_ },
+ { "fingerprint", fingerprint_ },
+ },
+ };
+
+ std::string proto = ConstructProto(partitions);
+ ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_empty_fingerprint) {
+ // This test relies on dm-verity support.
+ if (!verity_supported) {
+ GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+ return;
+ }
+
+ std::vector<std::unordered_map<std::string, std::string>> partitions = {
+ {
+ { "name", "system" },
+ { "ranges", "2,0,1" },
+ },
+ };
+
+ std::string proto = ConstructProto(partitions);
+ ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
+}
+
+TEST_F(UpdateVerifierTest, verify_image_protobuf_fingerprint_mismatch) {
+ // This test relies on dm-verity support.
+ if (!verity_supported) {
+ GTEST_LOG_(INFO) << "Test skipped on devices without dm-verity support.";
+ return;
+ }
+
+ std::vector<std::unordered_map<std::string, std::string>> partitions = {
+ {
+ { "name", "system" },
+ { "ranges", "2,0,1" },
+ { "id", property_id_ },
+ { "fingerprint", "unsupported_fingerprint" },
+ },
+ };
+
+ std::string proto = ConstructProto(partitions);
+ ASSERT_TRUE(android::base::WriteStringToFile(proto, care_map_pb_));
+ ASSERT_FALSE(verifier_.ParseCareMap());
}
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index 5bfd7cb..24c63e7 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -27,6 +27,8 @@
#include <vector>
#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
@@ -35,26 +37,33 @@
#include <brotli/encode.h>
#include <bsdiff/bsdiff.h>
#include <gtest/gtest.h>
+#include <verity/hash_tree_builder.h>
#include <ziparchive/zip_archive.h>
#include <ziparchive/zip_writer.h>
+#include "applypatch/applypatch.h"
#include "common/test_constants.h"
#include "edify/expr.h"
-#include "otautil/SysUtil.h"
-#include "otautil/cache_location.h"
#include "otautil/error_code.h"
+#include "otautil/paths.h"
#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
+#include "private/commands.h"
#include "updater/blockimg.h"
#include "updater/install.h"
#include "updater/updater.h"
-struct selabel_handle *sehandle = nullptr;
+using namespace std::string_literals;
-static void expect(const char* expected, const char* expr_str, CauseCode cause_code,
+using PackageEntries = std::unordered_map<std::string, std::string>;
+
+struct selabel_handle* sehandle = nullptr;
+
+static void expect(const char* expected, const std::string& expr_str, CauseCode cause_code,
UpdaterInfo* info = nullptr) {
std::unique_ptr<Expr> e;
int error_count = 0;
- ASSERT_EQ(0, parse_string(expr_str, &e, &error_count));
+ ASSERT_EQ(0, ParseString(expr_str, &e, &error_count));
ASSERT_EQ(0, error_count);
State state(expr_str, info);
@@ -65,7 +74,7 @@
if (expected == nullptr) {
ASSERT_FALSE(status);
} else {
- ASSERT_TRUE(status);
+ ASSERT_TRUE(status) << "Evaluate() finished with error message: " << state.errmsg;
ASSERT_STREQ(expected, result.c_str());
}
@@ -76,12 +85,12 @@
ASSERT_EQ(cause_code, state.cause_code);
}
-static void BuildUpdatePackage(const std::unordered_map<std::string, std::string>& entries,
- int fd) {
+static void BuildUpdatePackage(const PackageEntries& entries, int fd) {
FILE* zip_file_ptr = fdopen(fd, "wb");
ZipWriter zip_writer(zip_file_ptr);
for (const auto& entry : entries) {
+ // All the entries are written as STORED.
ASSERT_EQ(0, zip_writer.StartEntry(entry.first.c_str(), 0));
if (!entry.second.empty()) {
ASSERT_EQ(0, zip_writer.WriteBytes(entry.second.data(), entry.second.size()));
@@ -93,28 +102,103 @@
ASSERT_EQ(0, fclose(zip_file_ptr));
}
+static void RunBlockImageUpdate(bool is_verify, const PackageEntries& entries,
+ const std::string& image_file, const std::string& result,
+ CauseCode cause_code = kNoCause) {
+ CHECK(entries.find("transfer_list") != entries.end());
+
+ // Build the update package.
+ TemporaryFile zip_file;
+ BuildUpdatePackage(entries, zip_file.release());
+
+ MemMapping map;
+ ASSERT_TRUE(map.MapFile(zip_file.path));
+ ZipArchiveHandle handle;
+ ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+
+ // Set up the handler, command_pipe, patch offset & length.
+ UpdaterInfo updater_info;
+ updater_info.package_zip = handle;
+ TemporaryFile temp_pipe;
+ updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
+ updater_info.package_zip_addr = map.addr;
+ updater_info.package_zip_len = map.length;
+
+ std::string new_data = entries.find("new_data.br") != entries.end() ? "new_data.br" : "new_data";
+ std::string script = is_verify ? "block_image_verify" : "block_image_update";
+ script += R"((")" + image_file + R"(", package_extract_file("transfer_list"), ")" + new_data +
+ R"(", "patch_data"))";
+ expect(result.c_str(), script, cause_code, &updater_info);
+
+ ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
+ CloseArchive(handle);
+}
+
static std::string get_sha1(const std::string& content) {
uint8_t digest[SHA_DIGEST_LENGTH];
SHA1(reinterpret_cast<const uint8_t*>(content.c_str()), content.size(), digest);
return print_sha1(digest);
}
+static Value* BlobToString(const char* name, State* state,
+ const std::vector<std::unique_ptr<Expr>>& argv) {
+ if (argv.size() != 1) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size());
+ }
+
+ std::vector<std::unique_ptr<Value>> args;
+ if (!ReadValueArgs(state, argv, &args)) {
+ return nullptr;
+ }
+
+ if (args[0]->type != Value::Type::BLOB) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s() expects a BLOB argument", name);
+ }
+
+ args[0]->type = Value::Type::STRING;
+ return args[0].release();
+}
+
class UpdaterTest : public ::testing::Test {
protected:
- virtual void SetUp() override {
+ void SetUp() override {
RegisterBuiltins();
RegisterInstallFunctions();
RegisterBlockImageFunctions();
- // Mock the location of last_command_file.
- CacheLocation::location().set_cache_temp_source(temp_saved_source_.path);
- CacheLocation::location().set_last_command_file(temp_last_command_.path);
- CacheLocation::location().set_stash_directory_base(temp_stash_base_.path);
+ RegisterFunction("blob_to_string", BlobToString);
+
+ // Each test is run in a separate process (isolated mode). Shared temporary files won't cause
+ // conflicts.
+ Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+ Paths::Get().set_last_command_file(temp_last_command_.path);
+ Paths::Get().set_stash_directory_base(temp_stash_base_.path);
+
+ // Enable a special command "abort" to simulate interruption.
+ Command::abort_allowed_ = true;
+
+ last_command_file_ = temp_last_command_.path;
+ image_file_ = image_temp_file_.path;
+ }
+
+ void TearDown() override {
+ // Clean up the last_command_file if any.
+ ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+ // Clear partition updated marker if any.
+ std::string updated_marker{ temp_stash_base_.path };
+ updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+ ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
}
TemporaryFile temp_saved_source_;
- TemporaryFile temp_last_command_;
TemporaryDir temp_stash_base_;
+ std::string last_command_file_;
+ std::string image_file_;
+
+ private:
+ TemporaryFile temp_last_command_;
+ TemporaryFile image_temp_file_;
};
TEST_F(UpdaterTest, getprop) {
@@ -131,80 +215,47 @@
expect(nullptr, "getprop(\"arg1\", \"arg2\")", kArgsParsingFailure);
}
-TEST_F(UpdaterTest, sha1_check) {
- // sha1_check(data) returns the SHA-1 of the data.
- expect("81fe8bfe87576c3ecb22426f8e57847382917acf", "sha1_check(\"abcd\")", kNoCause);
- expect("da39a3ee5e6b4b0d3255bfef95601890afd80709", "sha1_check(\"\")", kNoCause);
+TEST_F(UpdaterTest, patch_partition_check) {
+ // Zero argument is not valid.
+ expect(nullptr, "patch_partition_check()", kArgsParsingFailure);
- // sha1_check(data, sha1_hex, [sha1_hex, ...]) returns the matched SHA-1.
- expect("81fe8bfe87576c3ecb22426f8e57847382917acf",
- "sha1_check(\"abcd\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")",
- kNoCause);
+ std::string source_file = from_testdata_base("boot.img");
+ std::string source_content;
+ ASSERT_TRUE(android::base::ReadFileToString(source_file, &source_content));
+ size_t source_size = source_content.size();
+ std::string source_hash = get_sha1(source_content);
+ Partition source(source_file, source_size, source_hash);
- expect("81fe8bfe87576c3ecb22426f8e57847382917acf",
- "sha1_check(\"abcd\", \"wrong_sha1\", \"81fe8bfe87576c3ecb22426f8e57847382917acf\")",
- kNoCause);
+ std::string target_file = from_testdata_base("recovery.img");
+ std::string target_content;
+ ASSERT_TRUE(android::base::ReadFileToString(target_file, &target_content));
+ size_t target_size = target_content.size();
+ std::string target_hash = get_sha1(target_content);
+ Partition target(target_file, target_size, target_hash);
- // Or "" if there's no match.
- expect("",
- "sha1_check(\"abcd\", \"wrong_sha1\")",
- kNoCause);
+ // One argument is not valid.
+ expect(nullptr, "patch_partition_check(\"" + source.ToString() + "\")", kArgsParsingFailure);
+ expect(nullptr, "patch_partition_check(\"" + target.ToString() + "\")", kArgsParsingFailure);
- expect("",
- "sha1_check(\"abcd\", \"wrong_sha1\", \"wrong_sha2\")",
- kNoCause);
+ // Both of the source and target have the desired checksum.
+ std::string cmd =
+ "patch_partition_check(\"" + source.ToString() + "\", \"" + target.ToString() + "\")";
+ expect("t", cmd, kNoCause);
- // sha1_check() expects at least one argument.
- expect(nullptr, "sha1_check()", kArgsParsingFailure);
-}
+ // Only source partition has the desired checksum.
+ Partition bad_target(target_file, target_size - 1, target_hash);
+ cmd = "patch_partition_check(\"" + source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+ expect("t", cmd, kNoCause);
-TEST_F(UpdaterTest, apply_patch_check) {
- // Zero-argument is not valid.
- expect(nullptr, "apply_patch_check()", kArgsParsingFailure);
+ // Only target partition has the desired checksum.
+ Partition bad_source(source_file, source_size + 1, source_hash);
+ cmd = "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + target.ToString() + "\")";
+ expect("t", cmd, kNoCause);
- // File not found.
- expect("", "apply_patch_check(\"/doesntexist\")", kNoCause);
-
- std::string src_file = from_testdata_base("old.file");
- std::string src_content;
- ASSERT_TRUE(android::base::ReadFileToString(src_file, &src_content));
- size_t src_size = src_content.size();
- std::string src_hash = get_sha1(src_content);
-
- // One-argument with EMMC:file:size:sha1 should pass the check.
- std::string filename = android::base::Join(
- std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size), src_hash }, ":");
- std::string cmd = "apply_patch_check(\"" + filename + "\")";
- expect("t", cmd.c_str(), kNoCause);
-
- // EMMC:file:(size-1):sha1:(size+1):sha1 should fail the check.
- std::string filename_bad = android::base::Join(
- std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1), src_hash,
- std::to_string(src_size + 1), src_hash },
- ":");
- cmd = "apply_patch_check(\"" + filename_bad + "\")";
- expect("", cmd.c_str(), kNoCause);
-
- // EMMC:file:(size-1):sha1:size:sha1:(size+1):sha1 should pass the check.
- filename_bad =
- android::base::Join(std::vector<std::string>{ "EMMC", src_file, std::to_string(src_size - 1),
- src_hash, std::to_string(src_size), src_hash,
- std::to_string(src_size + 1), src_hash },
- ":");
- cmd = "apply_patch_check(\"" + filename_bad + "\")";
- expect("t", cmd.c_str(), kNoCause);
-
- // Multiple arguments.
- cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"wrong_sha2\")";
- expect("", cmd.c_str(), kNoCause);
-
- cmd = "apply_patch_check(\"" + filename + "\", \"wrong_sha1\", \"" + src_hash +
- "\", \"wrong_sha2\")";
- expect("t", cmd.c_str(), kNoCause);
-
- cmd = "apply_patch_check(\"" + filename_bad + "\", \"wrong_sha1\", \"" + src_hash +
- "\", \"wrong_sha2\")";
- expect("t", cmd.c_str(), kNoCause);
+ // Neither of the source or target has the desired checksum.
+ cmd =
+ "patch_partition_check(\"" + bad_source.ToString() + "\", \"" + bad_target.ToString() + "\")";
+ expect("", cmd, kNoCause);
}
TEST_F(UpdaterTest, file_getprop) {
@@ -214,7 +265,7 @@
expect(nullptr, "file_getprop(\"arg1\", \"arg2\", \"arg3\")", kArgsParsingFailure);
// File doesn't exist.
- expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFileGetPropFailure);
+ expect(nullptr, "file_getprop(\"/doesntexist\", \"key1\")", kFreadFailure);
// Reject too large files (current limit = 65536).
TemporaryFile temp_file1;
@@ -231,28 +282,28 @@
std::string script1("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.name\")");
- expect("tardis", script1.c_str(), kNoCause);
+ expect("tardis", script1, kNoCause);
std::string script2("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.board\")");
- expect("magic", script2.c_str(), kNoCause);
+ expect("magic", script2, kNoCause);
// No match.
std::string script3("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.wrong\")");
- expect("", script3.c_str(), kNoCause);
+ expect("", script3, kNoCause);
std::string script4("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.name=\")");
- expect("", script4.c_str(), kNoCause);
+ expect("", script4, kNoCause);
std::string script5("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.nam\")");
- expect("", script5.c_str(), kNoCause);
+ expect("", script5, kNoCause);
std::string script6("file_getprop(\"" + std::string(temp_file2.path) +
"\", \"ro.product.model\")");
- expect("", script6.c_str(), kNoCause);
+ expect("", script6, kNoCause);
}
// TODO: Test extracting to block device.
@@ -272,7 +323,7 @@
// Two-argument version.
TemporaryFile temp_file1;
std::string script("package_extract_file(\"a.txt\", \"" + std::string(temp_file1.path) + "\")");
- expect("t", script.c_str(), kNoCause, &updater_info);
+ expect("t", script, kNoCause, &updater_info);
// Verify the extracted entry.
std::string data;
@@ -281,33 +332,135 @@
// Now extract another entry to the same location, which should overwrite.
script = "package_extract_file(\"b.txt\", \"" + std::string(temp_file1.path) + "\")";
- expect("t", script.c_str(), kNoCause, &updater_info);
+ expect("t", script, kNoCause, &updater_info);
ASSERT_TRUE(android::base::ReadFileToString(temp_file1.path, &data));
ASSERT_EQ(kBTxtContents, data);
// Missing zip entry. The two-argument version doesn't abort.
script = "package_extract_file(\"doesntexist\", \"" + std::string(temp_file1.path) + "\")";
- expect("", script.c_str(), kNoCause, &updater_info);
+ expect("", script, kNoCause, &updater_info);
// Extract to /dev/full should fail.
script = "package_extract_file(\"a.txt\", \"/dev/full\")";
- expect("", script.c_str(), kNoCause, &updater_info);
+ expect("", script, kNoCause, &updater_info);
- // One-argument version.
- script = "sha1_check(package_extract_file(\"a.txt\"))";
- expect(kATxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info);
+ // One-argument version. package_extract_file() gives a VAL_BLOB, which needs to be converted to
+ // VAL_STRING for equality test.
+ script = "blob_to_string(package_extract_file(\"a.txt\")) == \"" + kATxtContents + "\"";
+ expect("t", script, kNoCause, &updater_info);
- script = "sha1_check(package_extract_file(\"b.txt\"))";
- expect(kBTxtSha1Sum.c_str(), script.c_str(), kNoCause, &updater_info);
+ script = "blob_to_string(package_extract_file(\"b.txt\")) == \"" + kBTxtContents + "\"";
+ expect("t", script, kNoCause, &updater_info);
// Missing entry. The one-argument version aborts the evaluation.
script = "package_extract_file(\"doesntexist\")";
- expect(nullptr, script.c_str(), kPackageExtractFileFailure, &updater_info);
+ expect(nullptr, script, kPackageExtractFileFailure, &updater_info);
CloseArchive(handle);
}
+TEST_F(UpdaterTest, read_file) {
+ // read_file() expects one argument.
+ expect(nullptr, "read_file()", kArgsParsingFailure);
+ expect(nullptr, "read_file(\"arg1\", \"arg2\")", kArgsParsingFailure);
+
+ // Write some value to file and read back.
+ TemporaryFile temp_file;
+ std::string script("write_value(\"foo\", \""s + temp_file.path + "\");");
+ expect("t", script, kNoCause);
+
+ script = "read_file(\""s + temp_file.path + "\") == \"foo\"";
+ expect("t", script, kNoCause);
+
+ script = "read_file(\""s + temp_file.path + "\") == \"bar\"";
+ expect("", script, kNoCause);
+
+ // It should fail gracefully when read fails.
+ script = "read_file(\"/doesntexist\")";
+ expect("", script, kNoCause);
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_smoke) {
+ std::string data;
+ for (unsigned char i = 0; i < 128; i++) {
+ data += std::string(4096, i);
+ }
+ // Appends an additional block for verity data.
+ data += std::string(4096, 0);
+ ASSERT_EQ(129 * 4096, data.size());
+ ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+ std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+ std::string expected_root_hash =
+ "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+ // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+ std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+ expected_root_hash };
+ std::string hash_tree_command = android::base::Join(tokens, " ");
+
+ std::vector<std::string> transfer_list{
+ "4", "2", "0", "2", hash_tree_command,
+ };
+
+ PackageEntries entries{
+ { "new_data", "" },
+ { "patch_data", "" },
+ { "transfer_list", android::base::Join(transfer_list, "\n") },
+ };
+
+ RunBlockImageUpdate(false, entries, image_file_, "t");
+
+ std::string updated;
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated));
+ ASSERT_EQ(129 * 4096, updated.size());
+ ASSERT_EQ(data.substr(0, 128 * 4096), updated.substr(0, 128 * 4096));
+
+ // Computes the SHA256 of the salt + hash_tree_data and expects the result to match with the
+ // root_hash.
+ std::vector<unsigned char> salt_bytes;
+ ASSERT_TRUE(HashTreeBuilder::ParseBytesArrayFromString(salt, &salt_bytes));
+ std::vector<unsigned char> hash_tree = std::move(salt_bytes);
+ hash_tree.insert(hash_tree.end(), updated.begin() + 128 * 4096, updated.end());
+
+ std::vector<unsigned char> digest(SHA256_DIGEST_LENGTH);
+ SHA256(hash_tree.data(), hash_tree.size(), digest.data());
+ ASSERT_EQ(expected_root_hash, HashTreeBuilder::BytesArrayToString(digest));
+}
+
+TEST_F(UpdaterTest, compute_hash_tree_root_mismatch) {
+ std::string data;
+ for (size_t i = 0; i < 128; i++) {
+ data += std::string(4096, i);
+ }
+ // Appends an additional block for verity data.
+ data += std::string(4096, 0);
+ ASSERT_EQ(129 * 4096, data.size());
+ // Corrupts one bit
+ data[4096] = 'A';
+ ASSERT_TRUE(android::base::WriteStringToFile(data, image_file_));
+
+ std::string salt = "aee087a5be3b982978c923f566a94613496b417f2af592639bc80d141e34dfe7";
+ std::string expected_root_hash =
+ "7e0a8d8747f54384014ab996f5b2dc4eb7ff00c630eede7134c9e3f05c0dd8ca";
+ // hash_tree_ranges, source_ranges, hash_algorithm, salt_hex, root_hash
+ std::vector<std::string> tokens{ "compute_hash_tree", "2,128,129", "2,0,128", "sha256", salt,
+ expected_root_hash };
+ std::string hash_tree_command = android::base::Join(tokens, " ");
+
+ std::vector<std::string> transfer_list{
+ "4", "2", "0", "2", hash_tree_command,
+ };
+
+ PackageEntries entries{
+ { "new_data", "" },
+ { "patch_data", "" },
+ { "transfer_list", android::base::Join(transfer_list, "\n") },
+ };
+
+ RunBlockImageUpdate(false, entries, image_file_, "", kHashTreeComputationFailure);
+}
+
TEST_F(UpdaterTest, write_value) {
// write_value() expects two arguments.
expect(nullptr, "write_value()", kArgsParsingFailure);
@@ -321,7 +474,7 @@
TemporaryFile temp_file;
std::string value = "magicvalue";
std::string script("write_value(\"" + value + "\", \"" + std::string(temp_file.path) + "\")");
- expect("t", script.c_str(), kNoCause);
+ expect("t", script, kNoCause);
// Verify the content.
std::string content;
@@ -330,7 +483,7 @@
// Allow writing empty string.
script = "write_value(\"\", \"" + std::string(temp_file.path) + "\")";
- expect("t", script.c_str(), kNoCause);
+ expect("t", script, kNoCause);
// Verify the content.
ASSERT_TRUE(android::base::ReadFileToString(temp_file.path, &content));
@@ -338,7 +491,7 @@
// It should fail gracefully when write fails.
script = "write_value(\"value\", \"/proc/0/file1\")";
- expect("", script.c_str(), kNoCause);
+ expect("", script, kNoCause);
}
TEST_F(UpdaterTest, get_stage) {
@@ -357,11 +510,11 @@
// Can read the stage value.
std::string script("get_stage(\"" + temp_file + "\")");
- expect("2/3", script.c_str(), kNoCause);
+ expect("2/3", script, kNoCause);
// Bad BCB path.
script = "get_stage(\"doesntexist\")";
- expect("", script.c_str(), kNoCause);
+ expect("", script, kNoCause);
}
TEST_F(UpdaterTest, set_stage) {
@@ -381,7 +534,7 @@
// Write with set_stage().
std::string script("set_stage(\"" + temp_file + "\", \"1/3\")");
- expect(tf.path, script.c_str(), kNoCause);
+ expect(tf.path, script, kNoCause);
// Verify.
bootloader_message boot_verify;
@@ -393,10 +546,10 @@
// Bad BCB path.
script = "set_stage(\"doesntexist\", \"1/3\")";
- expect("", script.c_str(), kNoCause);
+ expect("", script, kNoCause);
script = "set_stage(\"/dev/full\", \"1/3\")";
- expect("", script.c_str(), kNoCause);
+ expect("", script, kNoCause);
}
TEST_F(UpdaterTest, set_progress) {
@@ -448,22 +601,42 @@
ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
}
+TEST_F(UpdaterTest, block_image_update_parsing_error) {
+ std::vector<std::string> transfer_list{
+ // clang-format off
+ "4",
+ "2",
+ "0",
+ // clang-format on
+ };
+
+ PackageEntries entries{
+ { "new_data", "" },
+ { "patch_data", "" },
+ { "transfer_list", android::base::Join(transfer_list, '\n') },
+ };
+
+ RunBlockImageUpdate(false, entries, image_file_, "", kArgsParsingFailure);
+}
+
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');
// 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*>(src_content.data()), src_content.size(),
+ reinterpret_cast<const uint8_t*>(tgt_content.data()), tgt_content.size(),
+ patch_file.path, nullptr));
std::string patch_content;
ASSERT_TRUE(android::base::ReadFileToString(patch_file.path, &patch_content));
// Create the transfer list that contains a bsdiff.
std::string src_hash = get_sha1(src_content);
std::string tgt_hash = get_sha1(tgt_content);
- std::vector<std::string> transfer_list = {
+ std::vector<std::string> transfer_list{
+ // clang-format off
"4",
"2",
"0",
@@ -472,183 +645,108 @@
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,
+ // clang-format on
};
- std::unordered_map<std::string, std::string> entries = {
+ PackageEntries entries{
{ "new_data", "" },
{ "patch_data", patch_content },
{ "transfer_list", android::base::Join(transfer_list, '\n') },
};
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+ RunBlockImageUpdate(false, entries, image_file_, "t");
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
-
- // Execute the commands in the transfer list.
- TemporaryFile update_file;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- std::string script = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
- expect("t", script.c_str(), kNoCause, &updater_info);
// The update_file should be patched correctly.
std::string updated_content;
- ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
- ASSERT_EQ(tgt_hash, get_sha1(updated_content));
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content));
+ ASSERT_EQ(tgt_content, updated_content);
}
TEST_F(UpdaterTest, block_image_update_fail) {
std::string src_content(4096 * 2, 'e');
std::string src_hash = get_sha1(src_content);
// Stash and free some blocks, then fail the update intentionally.
- std::vector<std::string> transfer_list = {
- "4", "2", "0", "2", "stash " + src_hash + " 2,0,2", "free " + src_hash, "fail",
+ std::vector<std::string> transfer_list{
+ // clang-format off
+ "4",
+ "2",
+ "0",
+ "2",
+ "stash " + src_hash + " 2,0,2",
+ "free " + src_hash,
+ "abort",
+ // clang-format on
};
// Add a new data of 10 bytes to test the deadlock.
- std::unordered_map<std::string, std::string> entries = {
+ PackageEntries entries{
{ "new_data", std::string(10, 0) },
{ "patch_data", "" },
{ "transfer_list", android::base::Join(transfer_list, '\n') },
};
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ ASSERT_TRUE(android::base::WriteStringToFile(src_content, image_file_));
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+ RunBlockImageUpdate(false, entries, image_file_, "");
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
-
- TemporaryFile update_file;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- // Expect the stashed blocks to be freed.
- std::string script = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
- expect("", script.c_str(), kNoCause, &updater_info);
// Updater generates the stash name based on the input file name.
- std::string name_digest = get_sha1(update_file.path);
+ std::string name_digest = get_sha1(image_file_);
std::string stash_base = std::string(temp_stash_base_.path) + "/" + name_digest;
ASSERT_EQ(0, access(stash_base.c_str(), F_OK));
+ // Expect the stashed blocks to be freed.
ASSERT_EQ(-1, access((stash_base + src_hash).c_str(), F_OK));
ASSERT_EQ(0, rmdir(stash_base.c_str()));
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
}
TEST_F(UpdaterTest, new_data_over_write) {
- std::vector<std::string> transfer_list = {
- "4", "1", "0", "0", "new 2,0,1",
- };
-
- // Write 4096 + 100 bytes of new data.
- std::unordered_map<std::string, std::string> entries = {
- { "new_data", std::string(4196, 0) },
- { "patch_data", "" },
- { "transfer_list", android::base::Join(transfer_list, '\n') },
- };
-
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
-
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
-
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
-
- TemporaryFile update_file;
- std::string script = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "new_data", "patch_data"))";
- expect("t", script.c_str(), kNoCause, &updater_info);
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
-}
-
-TEST_F(UpdaterTest, new_data_short_write) {
- std::vector<std::string> transfer_list = {
+ std::vector<std::string> transfer_list{
+ // clang-format off
"4",
"1",
"0",
"0",
"new 2,0,1",
+ // clang-format on
};
- std::unordered_map<std::string, std::string> entries = {
- { "empty_new_data", "" },
- { "short_new_data", std::string(10, 'a') },
- { "exact_new_data", std::string(4096, 'a') },
+ // Write 4096 + 100 bytes of new data.
+ PackageEntries entries{
+ { "new_data", std::string(4196, 0) },
{ "patch_data", "" },
{ "transfer_list", android::base::Join(transfer_list, '\n') },
};
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ RunBlockImageUpdate(false, entries, image_file_, "t");
+}
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+TEST_F(UpdaterTest, new_data_short_write) {
+ std::vector<std::string> transfer_list{
+ // clang-format off
+ "4",
+ "1",
+ "0",
+ "0",
+ "new 2,0,1",
+ // clang-format on
+ };
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
+ PackageEntries entries{
+ { "patch_data", "" },
+ { "transfer_list", android::base::Join(transfer_list, '\n') },
+ };
// Updater should report the failure gracefully rather than stuck in deadlock.
- TemporaryFile update_file;
- std::string script_empty_data = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "empty_new_data", "patch_data"))";
- expect("", script_empty_data.c_str(), kNoCause, &updater_info);
+ entries["new_data"] = "";
+ RunBlockImageUpdate(false, entries, image_file_, "");
- std::string script_short_data = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "short_new_data", "patch_data"))";
- expect("", script_short_data.c_str(), kNoCause, &updater_info);
+ entries["new_data"] = std::string(10, 'a');
+ RunBlockImageUpdate(false, entries, image_file_, "");
// Expect to write 1 block of new data successfully.
- std::string script_exact_data = "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "exact_new_data", "patch_data"))";
- expect("t", script_exact_data.c_str(), kNoCause, &updater_info);
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
+ entries["new_data"] = std::string(4096, 'a');
+ RunBlockImageUpdate(false, entries, image_file_, "t");
}
TEST_F(UpdaterTest, brotli_new_data) {
@@ -681,55 +779,30 @@
"new 2,99,100",
};
- std::unordered_map<std::string, std::string> entries = {
- { "new.dat.br", std::move(encoded_data) },
+ PackageEntries entries{
+ { "new_data.br", std::move(encoded_data) },
{ "patch_data", "" },
{ "transfer_list", android::base::Join(transfer_list, '\n') },
};
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
-
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
-
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wb");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
-
- // Check if we can decompress the new data correctly.
- TemporaryFile update_file;
- std::string script_new_data =
- "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list"), "new.dat.br", "patch_data"))";
- expect("t", script_new_data.c_str(), kNoCause, &updater_info);
+ RunBlockImageUpdate(false, entries, image_file_, "t");
std::string updated_content;
- ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_content));
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_content));
ASSERT_EQ(brotli_new_data, updated_content);
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
}
TEST_F(UpdaterTest, last_command_update) {
- std::string last_command_file = CacheLocation::location().last_command_file();
-
- std::string block1 = std::string(4096, '1');
- std::string block2 = std::string(4096, '2');
- std::string block3 = std::string(4096, '3');
+ std::string block1(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);
// Compose the transfer list to fail the first update.
- std::vector<std::string> transfer_list_fail = {
+ std::vector<std::string> transfer_list_fail{
+ // clang-format off
"4",
"2",
"0",
@@ -737,11 +810,13 @@
"stash " + block1_hash + " 2,0,1",
"move " + block1_hash + " 2,1,2 1 2,0,1",
"stash " + block3_hash + " 2,2,3",
- "fail",
+ "abort",
+ // clang-format on
};
// Mimic a resumed update with the same transfer commands.
- std::vector<std::string> transfer_list_continue = {
+ std::vector<std::string> transfer_list_continue{
+ // clang-format off
"4",
"2",
"0",
@@ -750,127 +825,88 @@
"move " + block1_hash + " 2,1,2 1 2,0,1",
"stash " + block3_hash + " 2,2,3",
"move " + block1_hash + " 2,2,3 1 2,0,1",
+ // clang-format on
};
- std::unordered_map<std::string, std::string> entries = {
+ ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+
+ PackageEntries entries{
{ "new_data", "" },
{ "patch_data", "" },
- { "transfer_list_fail", android::base::Join(transfer_list_fail, '\n') },
- { "transfer_list_continue", android::base::Join(transfer_list_continue, '\n') },
+ { "transfer_list", android::base::Join(transfer_list_fail, '\n') },
};
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ // "2\nstash " + block3_hash + " 2,2,3"
+ std::string last_command_content =
+ "2\n" + transfer_list_fail[TransferList::kTransferListHeaderLines + 2];
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
-
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
-
- std::string src_content = block1 + block2 + block3;
- TemporaryFile update_file;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- std::string script =
- "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list_fail"), "new_data", "patch_data"))";
- expect("", script.c_str(), kNoCause, &updater_info);
+ RunBlockImageUpdate(false, entries, image_file_, "");
// Expect last_command to contain the last stash command.
- std::string last_command_content;
- ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content));
- EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content);
+ std::string last_command_actual;
+ ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+ EXPECT_EQ(last_command_content, last_command_actual);
+
std::string updated_contents;
- ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents));
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents));
ASSERT_EQ(block1 + block1 + block3, updated_contents);
- // Resume the update, expect the first 'move' to be skipped but the second 'move' to be executed.
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- std::string script_second_update =
- "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list_continue"), "new_data", "patch_data"))";
- expect("t", script_second_update.c_str(), kNoCause, &updater_info);
- ASSERT_TRUE(android::base::ReadFileToString(update_file.path, &updated_contents));
- ASSERT_EQ(block1 + block2 + block1, updated_contents);
+ // "Resume" the update. Expect the first 'move' to be skipped but the second 'move' to be
+ // executed. Note that we intentionally reset the image file.
+ entries["transfer_list"] = android::base::Join(transfer_list_continue, '\n');
+ ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+ RunBlockImageUpdate(false, entries, image_file_, "t");
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_contents));
+ ASSERT_EQ(block1 + block2 + block1, updated_contents);
}
TEST_F(UpdaterTest, last_command_update_unresumable) {
- std::string last_command_file = CacheLocation::location().last_command_file();
-
- std::string block1 = std::string(4096, '1');
- std::string block2 = std::string(4096, '2');
+ std::string block1(4096, '1');
+ std::string block2(4096, '2');
std::string block1_hash = get_sha1(block1);
std::string block2_hash = get_sha1(block2);
// Construct an unresumable update with source blocks mismatch.
- std::vector<std::string> transfer_list_unresumable = {
- "4", "2", "0", "2", "stash " + block1_hash + " 2,0,1", "move " + block2_hash + " 2,1,2 1 2,0,1",
+ std::vector<std::string> transfer_list_unresumable{
+ // clang-format off
+ "4",
+ "2",
+ "0",
+ "2",
+ "stash " + block1_hash + " 2,0,1",
+ "move " + block2_hash + " 2,1,2 1 2,0,1",
+ // clang-format on
};
- std::unordered_map<std::string, std::string> entries = {
+ PackageEntries entries{
{ "new_data", "" },
{ "patch_data", "" },
- { "transfer_list_unresumable", android::base::Join(transfer_list_unresumable, '\n') },
+ { "transfer_list", android::base::Join(transfer_list_unresumable, '\n') },
};
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1, image_file_));
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+ std::string last_command_content =
+ "0\n" + transfer_list_unresumable[TransferList::kTransferListHeaderLines];
+ ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
+ RunBlockImageUpdate(false, entries, image_file_, "");
- // Set up the last_command_file
- ASSERT_TRUE(
- android::base::WriteStringToFile("0\nstash " + block1_hash + " 2,0,1", last_command_file));
-
- // The last_command_file will be deleted if the update encounters an unresumable failure
- // later.
- std::string src_content = block1 + block1;
- TemporaryFile update_file;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- std::string script =
- "block_image_update(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list_unresumable"), "new_data", "patch_data"))";
- expect("", script.c_str(), kNoCause, &updater_info);
- ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK));
-
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
+ // The last_command_file will be deleted if the update encounters an unresumable failure later.
+ ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
}
TEST_F(UpdaterTest, last_command_verify) {
- std::string last_command_file = CacheLocation::location().last_command_file();
-
- std::string block1 = std::string(4096, '1');
- std::string block2 = std::string(4096, '2');
- std::string block3 = std::string(4096, '3');
+ 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::vector<std::string> transfer_list_verify = {
+ std::vector<std::string> transfer_list_verify{
+ // clang-format off
"4",
"2",
"0",
@@ -879,55 +915,265 @@
"move " + block1_hash + " 2,0,1 1 2,0,1",
"move " + block1_hash + " 2,1,2 1 2,0,1",
"stash " + block3_hash + " 2,2,3",
+ // clang-format on
};
- std::unordered_map<std::string, std::string> entries = {
+ PackageEntries entries{
{ "new_data", "" },
{ "patch_data", "" },
- { "transfer_list_verify", android::base::Join(transfer_list_verify, '\n') },
+ { "transfer_list", android::base::Join(transfer_list_verify, '\n') },
};
- // Build the update package.
- TemporaryFile zip_file;
- BuildUpdatePackage(entries, zip_file.release());
+ ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1 + block3, image_file_));
- MemMapping map;
- ASSERT_TRUE(map.MapFile(zip_file.path));
- ZipArchiveHandle handle;
- ASSERT_EQ(0, OpenArchiveFromMemory(map.addr, map.length, zip_file.path, &handle));
+ // Last command: "move " + block1_hash + " 2,1,2 1 2,0,1"
+ std::string last_command_content =
+ "2\n" + transfer_list_verify[TransferList::kTransferListHeaderLines + 2];
- // Set up the handler, command_pipe, patch offset & length.
- UpdaterInfo updater_info;
- updater_info.package_zip = handle;
- TemporaryFile temp_pipe;
- updater_info.cmd_pipe = fdopen(temp_pipe.release(), "wbe");
- updater_info.package_zip_addr = map.addr;
- updater_info.package_zip_len = map.length;
+ // First run: expect the verification to succeed and the last_command_file is intact.
+ ASSERT_TRUE(android::base::WriteStringToFile(last_command_content, last_command_file_));
- std::string src_content = block1 + block1 + block3;
- TemporaryFile update_file;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
+ RunBlockImageUpdate(true, entries, image_file_, "t");
- ASSERT_TRUE(
- android::base::WriteStringToFile("2\nstash " + block3_hash + " 2,2,3", last_command_file));
+ std::string last_command_actual;
+ ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+ EXPECT_EQ(last_command_content, last_command_actual);
- // Expect the verification to succeed and the last_command_file is intact.
- std::string script_verify =
- "block_image_verify(\"" + std::string(update_file.path) +
- R"(", package_extract_file("transfer_list_verify"), "new_data","patch_data"))";
- expect("t", script_verify.c_str(), kNoCause, &updater_info);
+ // Second run with a mismatching block image: expect the verification to succeed but
+ // last_command_file to be deleted; because the target blocks in the last command don't have the
+ // expected contents for the second move command.
+ ASSERT_TRUE(android::base::WriteStringToFile(block1 + block2 + block3, image_file_));
+ RunBlockImageUpdate(true, entries, image_file_, "t");
+ ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+}
- std::string last_command_content;
- ASSERT_TRUE(android::base::ReadFileToString(last_command_file.c_str(), &last_command_content));
- EXPECT_EQ("2\nstash " + block3_hash + " 2,2,3", last_command_content);
+class ResumableUpdaterTest : public testing::TestWithParam<size_t> {
+ protected:
+ void SetUp() override {
+ RegisterBuiltins();
+ RegisterInstallFunctions();
+ RegisterBlockImageFunctions();
- // Expect the verification to succeed but last_command_file to be deleted; because the target
- // blocks don't have the expected contents for the second move command.
- src_content = block1 + block2 + block3;
- ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path));
- expect("t", script_verify.c_str(), kNoCause, &updater_info);
- ASSERT_EQ(-1, access(last_command_file.c_str(), R_OK));
+ Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+ Paths::Get().set_last_command_file(temp_last_command_.path);
+ Paths::Get().set_stash_directory_base(temp_stash_base_.path);
- ASSERT_EQ(0, fclose(updater_info.cmd_pipe));
- CloseArchive(handle);
+ // Enable a special command "abort" to simulate interruption.
+ Command::abort_allowed_ = true;
+
+ index_ = GetParam();
+ image_file_ = image_temp_file_.path;
+ last_command_file_ = temp_last_command_.path;
+ }
+
+ void TearDown() override {
+ // Clean up the last_command_file if any.
+ ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+ // Clear partition updated marker if any.
+ std::string updated_marker{ temp_stash_base_.path };
+ updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+ ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
+ }
+
+ TemporaryFile temp_saved_source_;
+ TemporaryDir temp_stash_base_;
+ std::string last_command_file_;
+ std::string image_file_;
+ size_t index_;
+
+ private:
+ TemporaryFile temp_last_command_;
+ TemporaryFile image_temp_file_;
+};
+
+static std::string g_source_image;
+static std::string g_target_image;
+static PackageEntries g_entries;
+
+static std::vector<std::string> GenerateTransferList() {
+ std::string a(4096, 'a');
+ std::string b(4096, 'b');
+ std::string c(4096, 'c');
+ std::string d(4096, 'd');
+ std::string e(4096, 'e');
+ std::string f(4096, 'f');
+ std::string g(4096, 'g');
+ std::string h(4096, 'h');
+ std::string i(4096, 'i');
+ std::string zero(4096, '\0');
+
+ std::string a_hash = get_sha1(a);
+ std::string b_hash = get_sha1(b);
+ std::string c_hash = get_sha1(c);
+ std::string e_hash = get_sha1(e);
+
+ auto loc = [](const std::string& range_text) {
+ std::vector<std::string> pieces = android::base::Split(range_text, "-");
+ size_t left;
+ size_t right;
+ if (pieces.size() == 1) {
+ CHECK(android::base::ParseUint(pieces[0], &left));
+ right = left + 1;
+ } else {
+ CHECK_EQ(2u, pieces.size());
+ CHECK(android::base::ParseUint(pieces[0], &left));
+ CHECK(android::base::ParseUint(pieces[1], &right));
+ right++;
+ }
+ return android::base::StringPrintf("2,%zu,%zu", left, right);
+ };
+
+ // patch 1: "b d c" -> "g"
+ TemporaryFile patch_file_bdc_g;
+ std::string bdc = b + d + c;
+ std::string bdc_hash = get_sha1(bdc);
+ std::string g_hash = get_sha1(g);
+ CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(),
+ reinterpret_cast<const uint8_t*>(g.data()), g.size(),
+ patch_file_bdc_g.path, nullptr));
+ std::string patch_bdc_g;
+ CHECK(android::base::ReadFileToString(patch_file_bdc_g.path, &patch_bdc_g));
+
+ // patch 2: "a b c d" -> "d c b"
+ TemporaryFile patch_file_abcd_dcb;
+ std::string abcd = a + b + c + d;
+ std::string abcd_hash = get_sha1(abcd);
+ std::string dcb = d + c + b;
+ std::string dcb_hash = get_sha1(dcb);
+ CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(),
+ reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(),
+ patch_file_abcd_dcb.path, nullptr));
+ std::string patch_abcd_dcb;
+ CHECK(android::base::ReadFileToString(patch_file_abcd_dcb.path, &patch_abcd_dcb));
+
+ std::vector<std::string> transfer_list{
+ "4",
+ "10", // total blocks written
+ "2", // maximum stash entries
+ "2", // maximum number of stashed blocks
+
+ // a b c d e a b c d e
+ "stash " + b_hash + " " + loc("1"),
+ // a b c d e a b c d e [b(1)]
+ "stash " + c_hash + " " + loc("2"),
+ // a b c d e a b c d e [b(1)][c(2)]
+ "new " + loc("1-2"),
+ // a i h d e a b c d e [b(1)][c(2)]
+ "zero " + loc("0"),
+ // 0 i h d e a b c d e [b(1)][c(2)]
+
+ // bsdiff "b d c" (from stash, 3, stash) to get g(3)
+ android::base::StringPrintf(
+ "bsdiff 0 %zu %s %s %s 3 %s %s %s:%s %s:%s",
+ patch_bdc_g.size(), // patch start (0), patch length
+ bdc_hash.c_str(), // source hash
+ g_hash.c_str(), // target hash
+ loc("3").c_str(), // target range
+ loc("3").c_str(), loc("1").c_str(), // load "d" from block 3, into buffer at offset 1
+ b_hash.c_str(), loc("0").c_str(), // load "b" from stash, into buffer at offset 0
+ c_hash.c_str(), loc("2").c_str()), // load "c" from stash, into buffer at offset 2
+
+ // 0 i h g e a b c d e [b(1)][c(2)]
+ "free " + b_hash,
+ // 0 i h g e a b c d e [c(2)]
+ "free " + a_hash,
+ // 0 i h g e a b c d e
+ "stash " + a_hash + " " + loc("5"),
+ // 0 i h g e a b c d e [a(5)]
+ "move " + e_hash + " " + loc("5") + " 1 " + loc("4"),
+ // 0 i h g e e b c d e [a(5)]
+
+ // bsdiff "a b c d" (from stash, 6-8) to "d c b" (6-8)
+ android::base::StringPrintf( //
+ "bsdiff %zu %zu %s %s %s 4 %s %s %s:%s",
+ patch_bdc_g.size(), // patch start
+ patch_bdc_g.size() + patch_abcd_dcb.size(), // patch length
+ abcd_hash.c_str(), // source hash
+ dcb_hash.c_str(), // target hash
+ loc("6-8").c_str(), // target range
+ loc("6-8").c_str(), // load "b c d" from blocks 6-8
+ loc("1-3").c_str(), // into buffer at offset 1-3
+ a_hash.c_str(), // load "a" from stash
+ loc("0").c_str()), // into buffer at offset 0
+
+ // 0 i h g e e d c b e [a(5)]
+ "new " + loc("4"),
+ // 0 i h g f e d c b e [a(5)]
+ "move " + a_hash + " " + loc("9") + " 1 - " + a_hash + ":" + loc("0"),
+ // 0 i h g f e d c b a [a(5)]
+ "free " + a_hash,
+ // 0 i h g f e d c b a
+ };
+
+ std::string new_data = i + h + f;
+ std::string patch_data = patch_bdc_g + patch_abcd_dcb;
+
+ g_entries = {
+ { "new_data", new_data },
+ { "patch_data", patch_data },
+ };
+ g_source_image = a + b + c + d + e + a + b + c + d + e;
+ g_target_image = zero + i + h + g + f + e + d + c + b + a;
+
+ return transfer_list;
+}
+
+static const std::vector<std::string> g_transfer_list = GenerateTransferList();
+
+INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest,
+ ::testing::Range(static_cast<size_t>(0),
+ g_transfer_list.size() -
+ TransferList::kTransferListHeaderLines));
+
+TEST_P(ResumableUpdaterTest, InterruptVerifyResume) {
+ ASSERT_TRUE(android::base::WriteStringToFile(g_source_image, image_file_));
+
+ LOG(INFO) << "Interrupting at line " << index_ << " ("
+ << g_transfer_list[TransferList::kTransferListHeaderLines + index_] << ")";
+
+ std::vector<std::string> transfer_list_copy{ g_transfer_list };
+ transfer_list_copy[TransferList::kTransferListHeaderLines + index_] = "abort";
+
+ g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n');
+
+ // Run update that's expected to fail.
+ RunBlockImageUpdate(false, g_entries, image_file_, "");
+
+ std::string last_command_expected;
+
+ // Assert the last_command_file.
+ if (index_ == 0) {
+ ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+ } else {
+ last_command_expected = std::to_string(index_ - 1) + "\n" +
+ g_transfer_list[TransferList::kTransferListHeaderLines + index_ - 1];
+ std::string last_command_actual;
+ ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+ ASSERT_EQ(last_command_expected, last_command_actual);
+ }
+
+ g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n');
+
+ // Resume the interrupted update, by doing verification first.
+ RunBlockImageUpdate(true, g_entries, image_file_, "t");
+
+ // last_command_file should remain intact.
+ if (index_ == 0) {
+ ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+ } else {
+ std::string last_command_actual;
+ ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+ ASSERT_EQ(last_command_expected, last_command_actual);
+ }
+
+ // Resume the update.
+ RunBlockImageUpdate(false, g_entries, image_file_, "t");
+
+ // last_command_file should be gone after successful update.
+ ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+
+ std::string updated_image_actual;
+ ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual));
+ ASSERT_EQ(g_target_image, updated_image_actual);
}
diff --git a/tests/component/verifier_test.cpp b/tests/component/verifier_test.cpp
index 2ef3828..3246ecd 100644
--- a/tests/component/verifier_test.cpp
+++ b/tests/component/verifier_test.cpp
@@ -16,7 +16,6 @@
#include <errno.h>
#include <fcntl.h>
-#include <gtest/gtest.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
@@ -28,9 +27,10 @@
#include <android-base/file.h>
#include <android-base/stringprintf.h>
#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
#include "common/test_constants.h"
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
#include "verifier.h"
using namespace std::string_literals;
diff --git a/tests/manual/recovery_test.cpp b/tests/manual/recovery_test.cpp
index 64e3b59..e1d0771 100644
--- a/tests/manual/recovery_test.cpp
+++ b/tests/manual/recovery_test.cpp
@@ -14,27 +14,22 @@
* limitations under the License.
*/
-#include <dirent.h>
+#include <errno.h>
+#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
+#include <memory>
#include <string>
-#include <vector>
#include <android-base/file.h>
-#include <android-base/strings.h>
#include <android/log.h>
#include <gtest/gtest.h>
-#include <png.h>
#include <private/android_logger.h>
-#include "minui/minui.h"
-
-static const std::string myFilename = "/data/misc/recovery/inject.txt";
-static const std::string myContent = "Hello World\nWelcome to my recovery\n";
-static const std::string kLocale = "zu";
-static const std::string kResourceTestDir = "/data/nativetest/recovery/";
+static const std::string kInjectTxtFilename = "/data/misc/recovery/inject.txt";
+static const std::string kInjectTxtContent = "Hello World\nWelcome to my recovery\n";
// Failure is expected on systems that do not deliver either the
// recovery-persist or recovery-refresh executables. Tests also require
@@ -44,9 +39,9 @@
const char *buf, size_t len, void *arg) {
EXPECT_EQ(LOG_ID_SYSTEM, logId);
EXPECT_EQ(ANDROID_LOG_INFO, prio);
- EXPECT_NE(std::string::npos, myFilename.find(filename));
- EXPECT_EQ(myContent, buf);
- EXPECT_EQ(myContent.size(), len);
+ EXPECT_NE(std::string::npos, kInjectTxtFilename.find(filename));
+ EXPECT_EQ(kInjectTxtContent, buf);
+ EXPECT_EQ(kInjectTxtContent.size(), len);
EXPECT_EQ(nullptr, arg);
return len;
}
@@ -59,13 +54,14 @@
ssize_t ret = __android_log_pmsg_file_read(
LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr);
if (ret == -ENOENT) {
- EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
- myFilename.c_str(), myContent.c_str(), myContent.size()));
+ EXPECT_LT(0, __android_log_pmsg_file_write(
+ LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(),
+ kInjectTxtContent.c_str(), kInjectTxtContent.size()));
- fprintf(stderr, "injected test data, requires two intervening reboots "
- "to check for replication\n");
+ fprintf(stderr,
+ "injected test data, requires two intervening reboots to check for replication\n");
}
- EXPECT_EQ(static_cast<ssize_t>(myContent.size()), ret);
+ EXPECT_EQ(static_cast<ssize_t>(kInjectTxtContent.size()), ret);
}
// recovery.persist - Requires recovery.inject, then a reboot, then
@@ -76,149 +72,18 @@
ssize_t ret = __android_log_pmsg_file_read(
LOG_ID_SYSTEM, ANDROID_LOG_INFO, "recovery/", __pmsg_fn, nullptr);
if (ret == -ENOENT) {
- EXPECT_LT(0, __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
- myFilename.c_str(), myContent.c_str(), myContent.size()));
+ EXPECT_LT(0, __android_log_pmsg_file_write(
+ LOG_ID_SYSTEM, ANDROID_LOG_INFO, kInjectTxtFilename.c_str(),
+ kInjectTxtContent.c_str(), kInjectTxtContent.size()));
- fprintf(stderr, "injected test data, requires intervening reboot "
- "to check for storage\n");
+ fprintf(stderr, "injected test data, requires intervening reboot to check for storage\n");
}
std::string buf;
- EXPECT_TRUE(android::base::ReadFileToString(myFilename, &buf));
- EXPECT_EQ(myContent, buf);
- if (access(myFilename.c_str(), F_OK) == 0) {
- fprintf(stderr, "Removing persistent test data, "
- "check if reconstructed on reboot\n");
+ EXPECT_TRUE(android::base::ReadFileToString(kInjectTxtFilename, &buf));
+ EXPECT_EQ(kInjectTxtContent, buf);
+ if (access(kInjectTxtFilename.c_str(), F_OK) == 0) {
+ fprintf(stderr, "Removing persistent test data, check if reconstructed on reboot\n");
}
- EXPECT_EQ(0, unlink(myFilename.c_str()));
+ EXPECT_EQ(0, unlink(kInjectTxtFilename.c_str()));
}
-
-std::vector<std::string> image_dir {
- "res-mdpi/images/",
- "res-hdpi/images/",
- "res-xhdpi/images/",
- "res-xxhdpi/images/",
- "res-xxxhdpi/images/"
-};
-
-static int png_filter(const dirent* de) {
- if (de->d_type != DT_REG || !android::base::EndsWith(de->d_name, "_text.png")) {
- return 0;
- }
- return 1;
-}
-
-// Find out all png files to test under /data/nativetest/recovery/.
-static std::vector<std::string> add_files() {
- std::vector<std::string> files;
- for (const std::string& str : image_dir) {
- std::string dir_path = kResourceTestDir + str;
- dirent** namelist;
- int n = scandir(dir_path.c_str(), &namelist, png_filter, alphasort);
- if (n == -1) {
- printf("Failed to scan dir %s: %s\n", kResourceTestDir.c_str(), strerror(errno));
- return files;
- }
- if (n == 0) {
- printf("No file is added for test in %s\n", kResourceTestDir.c_str());
- }
-
- while (n--) {
- std::string file_path = dir_path + namelist[n]->d_name;
- files.push_back(file_path);
- free(namelist[n]);
- }
- free(namelist);
- }
- return files;
-}
-
-class ResourceTest : public testing::TestWithParam<std::string> {
- public:
- static std::vector<std::string> png_list;
-
- // Parse a png file and test if it's qualified for the background text image
- // under recovery.
- void SetUp() override {
- std::string file_path = GetParam();
- fp = fopen(file_path.c_str(), "rbe");
- ASSERT_NE(nullptr, fp);
-
- unsigned char header[8];
- size_t bytesRead = fread(header, 1, sizeof(header), fp);
- ASSERT_EQ(sizeof(header), bytesRead);
- ASSERT_EQ(0, png_sig_cmp(header, 0, sizeof(header)));
-
- png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
- ASSERT_NE(nullptr, png_ptr);
-
- info_ptr = png_create_info_struct(png_ptr);
- ASSERT_NE(nullptr, info_ptr);
-
- png_init_io(png_ptr, fp);
- png_set_sig_bytes(png_ptr, sizeof(header));
- png_read_info(png_ptr, info_ptr);
-
- int color_type, bit_depth;
- png_get_IHDR(png_ptr, info_ptr, &width, &height, &bit_depth, &color_type, nullptr, nullptr,
- nullptr);
- ASSERT_EQ(PNG_COLOR_TYPE_GRAY, color_type) << "Recovery expects grayscale PNG file.";
- ASSERT_LT(static_cast<png_uint_32>(5), width);
- ASSERT_LT(static_cast<png_uint_32>(0), height);
- if (bit_depth <= 8) {
- // 1-, 2-, 4-, or 8-bit gray images: expand to 8-bit gray.
- png_set_expand_gray_1_2_4_to_8(png_ptr);
- }
-
- png_byte channels = png_get_channels(png_ptr, info_ptr);
- ASSERT_EQ(1, channels) << "Recovery background text images expects 1-channel PNG file.";
- }
-
- void TearDown() override {
- if (png_ptr != nullptr && info_ptr != nullptr) {
- png_destroy_read_struct(&png_ptr, &info_ptr, nullptr);
- }
-
- if (fp != nullptr) {
- fclose(fp);
- }
- }
-
- protected:
- png_structp png_ptr;
- png_infop info_ptr;
- png_uint_32 width, height;
-
- FILE* fp;
-};
-
-std::vector<std::string> ResourceTest::png_list = add_files();
-
-TEST_P(ResourceTest, ValidateLocale) {
- std::vector<unsigned char> row(width);
- for (png_uint_32 y = 0; y < height; ++y) {
- png_read_row(png_ptr, row.data(), nullptr);
- int w = (row[1] << 8) | row[0];
- int h = (row[3] << 8) | row[2];
- int len = row[4];
- EXPECT_LT(0, w);
- EXPECT_LT(0, h);
- EXPECT_LT(0, len) << "Locale string should be non-empty.";
- EXPECT_NE(0, row[5]) << "Locale string is missing.";
-
- ASSERT_GT(height, y + 1 + h) << "Locale: " << kLocale << " is not found in the file.";
- char* loc = reinterpret_cast<char*>(&row[5]);
- if (matches_locale(loc, kLocale.c_str())) {
- EXPECT_TRUE(android::base::StartsWith(loc, kLocale));
- break;
- } else {
- for (int i = 0; i < h; ++i, ++y) {
- png_read_row(png_ptr, row.data(), nullptr);
- }
- }
- }
-}
-
-INSTANTIATE_TEST_CASE_P(BackgroundTextValidation, ResourceTest,
- ::testing::ValuesIn(ResourceTest::png_list.cbegin(),
- ResourceTest::png_list.cend()));
diff --git a/tests/testdata/font.png b/tests/testdata/font.png
new file mode 100644
index 0000000..d95408a
--- /dev/null
+++ b/tests/testdata/font.png
Binary files differ
diff --git a/tests/testdata/gzipped_source b/tests/testdata/gzipped_source
new file mode 100644
index 0000000..6d425d0
--- /dev/null
+++ b/tests/testdata/gzipped_source
Binary files differ
diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target
new file mode 100644
index 0000000..5621262
--- /dev/null
+++ b/tests/testdata/gzipped_target
Binary files differ
diff --git a/tests/testdata/loop00000.png b/tests/testdata/loop00000.png
new file mode 100644
index 0000000..0e11c01
--- /dev/null
+++ b/tests/testdata/loop00000.png
Binary files differ
diff --git a/tests/testdata/new.file b/tests/testdata/new.file
deleted file mode 100644
index cdeb8fd..0000000
--- a/tests/testdata/new.file
+++ /dev/null
Binary files differ
diff --git a/tests/testdata/old.file b/tests/testdata/old.file
deleted file mode 100644
index 166c873..0000000
--- a/tests/testdata/old.file
+++ /dev/null
Binary files differ
diff --git a/tests/unit/applypatch_test.cpp b/tests/unit/applypatch_test.cpp
new file mode 100644
index 0000000..066f981
--- /dev/null
+++ b/tests/unit/applypatch_test.cpp
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agree to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <algorithm>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/test_utils.h>
+#include <android-base/unique_fd.h>
+#include <gtest/gtest.h>
+
+#include "applypatch/applypatch.h"
+#include "common/test_constants.h"
+#include "edify/expr.h"
+#include "otautil/paths.h"
+#include "otautil/print_sha1.h"
+
+using namespace std::string_literals;
+
+class ApplyPatchTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ source_file = from_testdata_base("boot.img");
+ FileContents boot_fc;
+ ASSERT_TRUE(LoadFileContents(source_file, &boot_fc));
+ source_size = boot_fc.data.size();
+ source_sha1 = print_sha1(boot_fc.sha1);
+
+ target_file = from_testdata_base("recovery.img");
+ FileContents recovery_fc;
+ ASSERT_TRUE(LoadFileContents(target_file, &recovery_fc));
+ target_size = recovery_fc.data.size();
+ target_sha1 = print_sha1(recovery_fc.sha1);
+
+ source_partition = Partition(source_file, source_size, source_sha1);
+ target_partition = Partition(partition_file.path, target_size, target_sha1);
+
+ srand(time(nullptr));
+ bad_sha1_a = android::base::StringPrintf("%040x", rand());
+ bad_sha1_b = android::base::StringPrintf("%040x", rand());
+
+ // Reset the cache backup file.
+ Paths::Get().set_cache_temp_source(cache_temp_source.path);
+ }
+
+ void TearDown() override {
+ ASSERT_TRUE(android::base::RemoveFileIfExists(cache_temp_source.path));
+ }
+
+ std::string source_file;
+ std::string source_sha1;
+ size_t source_size;
+
+ std::string target_file;
+ std::string target_sha1;
+ size_t target_size;
+
+ std::string bad_sha1_a;
+ std::string bad_sha1_b;
+
+ Partition source_partition;
+ Partition target_partition;
+
+ private:
+ TemporaryFile partition_file;
+ TemporaryFile cache_temp_source;
+};
+
+TEST_F(ApplyPatchTest, CheckPartition) {
+ ASSERT_TRUE(CheckPartition(source_partition));
+}
+
+TEST_F(ApplyPatchTest, CheckPartition_Mismatching) {
+ ASSERT_FALSE(CheckPartition(Partition(source_file, target_size, target_sha1)));
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size, bad_sha1_a)));
+
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size - 1, source_sha1)));
+ ASSERT_FALSE(CheckPartition(Partition(source_file, source_size + 1, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck) {
+ ASSERT_TRUE(PatchPartitionCheck(target_partition, source_partition));
+
+ ASSERT_TRUE(
+ PatchPartitionCheck(Partition(source_file, source_size - 1, source_sha1), source_partition));
+
+ ASSERT_TRUE(
+ PatchPartitionCheck(Partition(source_file, source_size + 1, source_sha1), source_partition));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup) {
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+ Paths::Get().set_cache_temp_source(source_file);
+ ASSERT_TRUE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartitionCheck_UseBackup_BothCorrupted) {
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+
+ Paths::Get().set_cache_temp_source(target_file);
+ ASSERT_FALSE(
+ PatchPartitionCheck(target_partition, Partition(target_file, source_size, source_sha1)));
+}
+
+TEST_F(ApplyPatchTest, PatchPartition) {
+ FileContents patch_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot.p"), &patch_fc));
+ Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+ FileContents bonus_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("bonus.file"), &bonus_fc));
+ Value bonus(Value::Type::BLOB, std::string(bonus_fc.data.cbegin(), bonus_fc.data.cend()));
+
+ ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, &bonus));
+}
+
+// Tests patching an eMMC target without a separate bonus file (i.e. recovery-from-boot patch has
+// everything).
+TEST_F(ApplyPatchTest, PatchPartitionWithoutBonusFile) {
+ FileContents patch_fc;
+ ASSERT_TRUE(LoadFileContents(from_testdata_base("recovery-from-boot-with-bonus.p"), &patch_fc));
+ Value patch(Value::Type::BLOB, std::string(patch_fc.data.cbegin(), patch_fc.data.cend()));
+
+ ASSERT_TRUE(PatchPartition(target_partition, source_partition, patch, nullptr));
+}
+
+class FreeCacheTest : public ::testing::Test {
+ protected:
+ static constexpr size_t PARTITION_SIZE = 4096 * 10;
+
+ // Returns a sorted list of files in |dirname|.
+ static std::vector<std::string> FindFilesInDir(const std::string& dirname) {
+ std::vector<std::string> file_list;
+
+ std::unique_ptr<DIR, decltype(&closedir)> d(opendir(dirname.c_str()), closedir);
+ struct dirent* de;
+ while ((de = readdir(d.get())) != 0) {
+ std::string path = dirname + "/" + de->d_name;
+
+ struct stat st;
+ if (stat(path.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
+ file_list.emplace_back(de->d_name);
+ }
+ }
+
+ std::sort(file_list.begin(), file_list.end());
+ return file_list;
+ }
+
+ void AddFilesToDir(const std::string& dir, const std::vector<std::string>& files) {
+ std::string zeros(4096, 0);
+ for (const auto& file : files) {
+ temporary_files_.push_back(dir + "/" + file);
+ ASSERT_TRUE(android::base::WriteStringToFile(zeros, temporary_files_.back()));
+ }
+ }
+
+ void SetUp() override {
+ Paths::Get().set_cache_log_directory(mock_log_dir.path);
+ temporary_files_.clear();
+ }
+
+ void TearDown() override {
+ for (const auto& file : temporary_files_) {
+ ASSERT_TRUE(android::base::RemoveFileIfExists(file));
+ }
+ }
+
+ // A mock method to calculate the free space. It assumes the partition has a total size of 40960
+ // bytes and all files are 4096 bytes in size.
+ static size_t MockFreeSpaceChecker(const std::string& dirname) {
+ std::vector<std::string> files = FindFilesInDir(dirname);
+ return PARTITION_SIZE - 4096 * files.size();
+ }
+
+ TemporaryDir mock_cache;
+ TemporaryDir mock_log_dir;
+
+ private:
+ std::vector<std::string> temporary_files_;
+};
+
+TEST_F(FreeCacheTest, FreeCacheSmoke) {
+ std::vector<std::string> files = { "file1", "file2", "file3" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_cache.path, MockFreeSpaceChecker));
+
+ ASSERT_EQ(std::vector<std::string>{ "file3" }, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheFreeSpaceCheckerError) {
+ std::vector<std::string> files{ "file1", "file2", "file3" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 7, MockFreeSpaceChecker(mock_cache.path));
+
+ ASSERT_FALSE(
+ RemoveFilesInDirectory(4096 * 9, mock_cache.path, [](const std::string&) { return -1; }));
+}
+
+TEST_F(FreeCacheTest, FreeCacheOpenFile) {
+ std::vector<std::string> files = { "file1", "file2" };
+ AddFilesToDir(mock_cache.path, files);
+ ASSERT_EQ(files, FindFilesInDir(mock_cache.path));
+ ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_cache.path));
+
+ std::string file1_path = mock_cache.path + "/file1"s;
+ android::base::unique_fd fd(open(file1_path.c_str(), O_RDONLY));
+
+ // file1 can't be deleted as it's opened by us.
+ ASSERT_FALSE(RemoveFilesInDirectory(4096 * 10, mock_cache.path, MockFreeSpaceChecker));
+
+ ASSERT_EQ(std::vector<std::string>{ "file1" }, FindFilesInDir(mock_cache.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsSmoke) {
+ std::vector<std::string> log_files = { "last_log", "last_log.1", "last_kmsg.2", "last_log.5",
+ "last_log.10" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Logs with a higher index will be deleted first
+ std::vector<std::string> expected = { "last_log", "last_log.1" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+ ASSERT_EQ(4096 * 8, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsStringComparison) {
+ std::vector<std::string> log_files = { "last_log.1", "last_kmsg.1", "last_log.not_number",
+ "last_kmsgrandom" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 6, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_TRUE(RemoveFilesInDirectory(4096 * 9, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Logs with incorrect format will be deleted first; and the last_kmsg with the same index is
+ // deleted before last_log.
+ std::vector<std::string> expected = { "last_log.1" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+ ASSERT_EQ(4096 * 9, MockFreeSpaceChecker(mock_log_dir.path));
+}
+
+TEST_F(FreeCacheTest, FreeCacheLogsOtherFiles) {
+ std::vector<std::string> log_files = { "last_install", "command", "block.map", "last_log",
+ "last_kmsg.1" };
+ AddFilesToDir(mock_log_dir.path, log_files);
+ ASSERT_EQ(4096 * 5, MockFreeSpaceChecker(mock_log_dir.path));
+
+ ASSERT_FALSE(RemoveFilesInDirectory(4096 * 8, mock_log_dir.path, MockFreeSpaceChecker));
+
+ // Non log files in /cache/recovery won't be deleted.
+ std::vector<std::string> expected = { "block.map", "command", "last_install" };
+ ASSERT_EQ(expected, FindFilesInDir(mock_log_dir.path));
+}
diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp
new file mode 100644
index 0000000..8a54df7
--- /dev/null
+++ b/tests/unit/commands_test.cpp
@@ -0,0 +1,554 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <algorithm>
+#include <string>
+
+#include <android-base/strings.h>
+#include <gtest/gtest.h>
+#include <openssl/sha.h>
+
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
+#include "private/commands.h"
+
+TEST(CommandsTest, ParseType) {
+ ASSERT_EQ(Command::Type::ZERO, Command::ParseType("zero"));
+ ASSERT_EQ(Command::Type::NEW, Command::ParseType("new"));
+ ASSERT_EQ(Command::Type::ERASE, Command::ParseType("erase"));
+ ASSERT_EQ(Command::Type::MOVE, Command::ParseType("move"));
+ ASSERT_EQ(Command::Type::BSDIFF, Command::ParseType("bsdiff"));
+ ASSERT_EQ(Command::Type::IMGDIFF, Command::ParseType("imgdiff"));
+ ASSERT_EQ(Command::Type::STASH, Command::ParseType("stash"));
+ ASSERT_EQ(Command::Type::FREE, Command::ParseType("free"));
+ ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, Command::ParseType("compute_hash_tree"));
+}
+
+TEST(CommandsTest, ParseType_InvalidCommand) {
+ ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo"));
+ ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar"));
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly) {
+ const std::vector<std::string> tokens{
+ "4,569884,569904,591946,592043",
+ "117",
+ "4,566779,566799,591946,592043",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+ target);
+ ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), {}, {}),
+ source);
+ ASSERT_EQ(117, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly) {
+ const std::vector<std::string> tokens{
+ "2,350729,350731",
+ "2",
+ "-",
+ "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15:2,0,2",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", &target,
+ "1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", &source, &err));
+ ASSERT_EQ(
+ TargetInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 350729, 350731 } })),
+ target);
+ ASSERT_EQ(
+ SourceInfo("1c25ba04d3278d6b65a1b9f17abac78425ec8b8d", {}, {},
+ {
+ StashInfo("6ebcf8cf1f6be0bc49e7d4a864214251925d1d15", RangeSet({ { 0, 2 } })),
+ }),
+ source);
+ ASSERT_EQ(2, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes) {
+ const std::vector<std::string> tokens{
+ "4,611641,611643,636981,637075",
+ "96",
+ "4,636981,637075,770665,770666",
+ "4,0,94,95,96",
+ "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_TRUE(Command::ParseTargetInfoAndSourceInfo(
+ tokens, "4734d1b241eb3d0f993714aaf7d665fae43772b6", &target,
+ "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d", &source, &err));
+ ASSERT_EQ(TargetInfo("4734d1b241eb3d0f993714aaf7d665fae43772b6",
+ RangeSet({ { 611641, 611643 }, { 636981, 637075 } })),
+ target);
+ ASSERT_EQ(SourceInfo(
+ "a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 636981, 637075 }, { 770665, 770666 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ {
+ StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23", RangeSet({ { 94, 95 } })),
+ }),
+ source);
+ ASSERT_EQ(96, source.blocks());
+}
+
+TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput) {
+ const std::vector<std::string> tokens{
+ "4,611641,611643,636981,637075",
+ "96",
+ "4,636981,637075,770665,770666",
+ "4,0,94,95,96",
+ "9eedf00d11061549e32503cadf054ec6fbfa7a23:2,94,95",
+ };
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+
+ // Mismatching block count.
+ {
+ std::vector<std::string> tokens_copy(tokens);
+ tokens_copy[1] = "97";
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+
+ // Excess stashes (causing block count mismatch).
+ {
+ std::vector<std::string> tokens_copy(tokens);
+ tokens_copy.push_back("e145a2f83a33334714ac65e34969c1f115e54a6f:2,0,22");
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ tokens_copy, "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+
+ // Invalid args.
+ for (size_t i = 0; i < tokens.size(); i++) {
+ TargetInfo target;
+ SourceInfo source;
+ std::string err;
+ ASSERT_FALSE(Command::ParseTargetInfoAndSourceInfo(
+ std::vector<std::string>(tokens.cbegin() + i + 1, tokens.cend()),
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &target,
+ "1d74d1a60332fd38cf9405f1bae67917888da6cb", &source, &err));
+ }
+}
+
+TEST(CommandsTest, Parse_EmptyInput) {
+ std::string err;
+ ASSERT_FALSE(Command::Parse("", 0, &err));
+ ASSERT_EQ("invalid type", err);
+}
+
+TEST(CommandsTest, Parse_ABORT_Allowed) {
+ Command::abort_allowed_ = true;
+
+ const std::string input{ "abort" };
+ std::string err;
+ Command command = Command::Parse(input, 0, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ABORT_NotAllowed) {
+ const std::string input{ "abort" };
+ std::string err;
+ Command command = Command::Parse(input, 0, &err);
+ ASSERT_FALSE(command);
+}
+
+TEST(CommandsTest, Parse_BSDIFF) {
+ const std::string input{
+ "bsdiff 0 148 "
+ "f201a4e04bd3860da6ad47b957ef424d58a58f8c 9d5d223b4bc5c45dbd25a799c4f1a98466731599 "
+ "4,565704,565752,566779,566799 "
+ "68 4,64525,64545,565704,565752"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 1, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::BSDIFF, command.type());
+ ASSERT_EQ(1, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("9d5d223b4bc5c45dbd25a799c4f1a98466731599",
+ RangeSet({ { 565704, 565752 }, { 566779, 566799 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("f201a4e04bd3860da6ad47b957ef424d58a58f8c",
+ RangeSet({ { 64525, 64545 }, { 565704, 565752 } }), RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(0, 148), command.patch());
+}
+
+TEST(CommandsTest, Parse_ERASE) {
+ const std::string input{ "erase 2,5,10" };
+ std::string err;
+ Command command = Command::Parse(input, 2, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::ERASE, command.type());
+ ASSERT_EQ(2, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 5, 10 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_FREE) {
+ const std::string input{ "free hash1" };
+ std::string err;
+ Command command = Command::Parse(input, 3, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::FREE, command.type());
+ ASSERT_EQ(3, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo("hash1", RangeSet()), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_IMGDIFF) {
+ const std::string input{
+ "imgdiff 29629269 185 "
+ "a6b1c49aed1b57a2aab1ec3e1505b945540cd8db 51978f65035f584a8ef7afa941dacb6d5e862164 "
+ "2,90851,90852 "
+ "1 2,90851,90852"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 4, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::IMGDIFF, command.type());
+ ASSERT_EQ(4, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("51978f65035f584a8ef7afa941dacb6d5e862164", RangeSet({ { 90851, 90852 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("a6b1c49aed1b57a2aab1ec3e1505b945540cd8db", RangeSet({ { 90851, 90852 } }),
+ RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(29629269, 185), command.patch());
+}
+
+TEST(CommandsTest, Parse_MOVE) {
+ const std::string input{
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb "
+ "4,569884,569904,591946,592043 117 4,566779,566799,591946,592043"
+ };
+ std::string err;
+ Command command = Command::Parse(input, 5, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::MOVE, command.type());
+ ASSERT_EQ(5, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 569884, 569904 }, { 591946, 592043 } })),
+ command.target());
+ ASSERT_EQ(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 566779, 566799 }, { 591946, 592043 } }), RangeSet(), {}),
+ command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_NEW) {
+ const std::string input{ "new 4,3,5,10,12" };
+ std::string err;
+ Command command = Command::Parse(input, 6, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::NEW, command.type());
+ ASSERT_EQ(6, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 3, 5 }, { 10, 12 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_STASH) {
+ const std::string input{ "stash hash1 2,5,10" };
+ std::string err;
+ Command command = Command::Parse(input, 7, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::STASH, command.type());
+ ASSERT_EQ(7, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo("hash1", RangeSet({ { 5, 10 } })), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_ZERO) {
+ const std::string input{ "zero 2,1,5" };
+ std::string err;
+ Command command = Command::Parse(input, 8, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::ZERO, command.type());
+ ASSERT_EQ(8, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ ASSERT_EQ(TargetInfo("unknown-hash", RangeSet({ { 1, 5 } })), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_COMPUTE_HASH_TREE) {
+ const std::string input{ "compute_hash_tree 2,0,1 2,3,4 sha1 unknown-salt unknown-root-hash" };
+ std::string err;
+ Command command = Command::Parse(input, 9, &err);
+ ASSERT_TRUE(command);
+
+ ASSERT_EQ(Command::Type::COMPUTE_HASH_TREE, command.type());
+ ASSERT_EQ(9, command.index());
+ ASSERT_EQ(input, command.cmdline());
+
+ HashTreeInfo expected_info(RangeSet({ { 0, 1 } }), RangeSet({ { 3, 4 } }), "sha1", "unknown-salt",
+ "unknown-root-hash");
+ ASSERT_EQ(expected_info, command.hash_tree_info());
+ ASSERT_EQ(TargetInfo(), command.target());
+ ASSERT_EQ(SourceInfo(), command.source());
+ ASSERT_EQ(StashInfo(), command.stash());
+ ASSERT_EQ(PatchInfo(), command.patch());
+}
+
+TEST(CommandsTest, Parse_InvalidNumberOfArgs) {
+ Command::abort_allowed_ = true;
+
+ // Note that the case of having excess args in BSDIFF, IMGDIFF and MOVE is covered by
+ // ParseTargetInfoAndSourceInfo_InvalidInput.
+ std::vector<std::string> inputs{
+ "abort foo",
+ "bsdiff",
+ "compute_hash_tree, 2,0,1 2,0,1 unknown-algorithm unknown-salt",
+ "erase",
+ "erase 4,3,5,10,12 hash1",
+ "free",
+ "free id1 id2",
+ "imgdiff",
+ "move",
+ "new",
+ "new 4,3,5,10,12 hash1",
+ "stash",
+ "stash id1",
+ "stash id1 4,3,5,10,12 id2",
+ "zero",
+ "zero 4,3,5,10,12 hash2",
+ };
+ for (const auto& input : inputs) {
+ std::string err;
+ ASSERT_FALSE(Command::Parse(input, 0, &err));
+ }
+}
+
+TEST(SourceInfoTest, Overlaps) {
+ ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+ ASSERT_TRUE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 4, 7 }, { 16, 23 } }))));
+
+ ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 9, 16 } }))));
+}
+
+TEST(SourceInfoTest, Overlaps_EmptySourceOrTarget) {
+ ASSERT_FALSE(SourceInfo().Overlaps(TargetInfo()));
+
+ ASSERT_FALSE(SourceInfo().Overlaps(
+ TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb", RangeSet({ { 7, 9 }, { 16, 20 } }))));
+
+ ASSERT_FALSE(SourceInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 7, 9 }, { 16, 20 } }), {}, {})
+ .Overlaps(TargetInfo()));
+}
+
+TEST(SourceInfoTest, Overlaps_WithStashes) {
+ ASSERT_FALSE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+ RangeSet({ { 94, 95 } })) })
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 175, 265 } }))));
+
+ ASSERT_TRUE(SourceInfo("a6cbdf3f416960f02189d3a814ec7e9e95c44a0d",
+ RangeSet({ { 81, 175 }, { 265, 266 } }), // source ranges
+ RangeSet({ { 0, 94 }, { 95, 96 } }), // source location
+ { StashInfo("9eedf00d11061549e32503cadf054ec6fbfa7a23",
+ RangeSet({ { 94, 95 } })) })
+ .Overlaps(TargetInfo("1d74d1a60332fd38cf9405f1bae67917888da6cb",
+ RangeSet({ { 265, 266 } }))));
+}
+
+// The block size should be specified by the caller of ReadAll (i.e. from Command instance during
+// normal run).
+constexpr size_t kBlockSize = 4096;
+
+TEST(SourceInfoTest, ReadAll) {
+ // "2727756cfee3fbfe24bf5650123fd7743d7b3465" is the SHA-1 hex digest of 8192 * 'a'.
+ const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+ {});
+ auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+ std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+ return 0;
+ };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+ ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(buffer.data(), buffer.size(), digest);
+ ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_WithStashes) {
+ const SourceInfo source(
+ // SHA-1 hex digest of 8192 * 'a' + 4096 * 'b'.
+ "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+ { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+ auto block_reader = [](const RangeSet& src, std::vector<uint8_t>* block_buffer) -> int {
+ std::fill_n(block_buffer->begin(), src.blocks() * kBlockSize, 'a');
+ return 0;
+ };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>* stash_buffer) -> int {
+ std::fill_n(stash_buffer->begin(), kBlockSize, 'b');
+ return 0;
+ };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ ASSERT_TRUE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+ ASSERT_EQ(source.blocks() * kBlockSize, buffer.size());
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(buffer.data(), buffer.size(), digest);
+ ASSERT_EQ(source.hash(), print_sha1(digest));
+}
+
+TEST(SourceInfoTest, ReadAll_BufferTooSmall) {
+ const SourceInfo source("2727756cfee3fbfe24bf5650123fd7743d7b3465", RangeSet({ { 0, 2 } }), {},
+ {});
+ auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize - 1);
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, stash_reader));
+}
+
+TEST(SourceInfoTest, ReadAll_FailingReader) {
+ const SourceInfo source(
+ "ee3ebea26130769c10ad13604712100346d48660", RangeSet({ { 0, 2 } }), RangeSet({ { 0, 2 } }),
+ { StashInfo("1e41f7a59e80c6eb4dc043caae80d273f130bed8", RangeSet({ { 2, 3 } })) });
+ std::vector<uint8_t> buffer(source.blocks() * kBlockSize);
+ auto failing_block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return -1; };
+ auto stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return 0; };
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, failing_block_reader, stash_reader));
+
+ auto block_reader = [](const RangeSet&, std::vector<uint8_t>*) -> int { return 0; };
+ auto failing_stash_reader = [](const std::string&, std::vector<uint8_t>*) -> int { return -1; };
+ ASSERT_FALSE(source.ReadAll(&buffer, kBlockSize, block_reader, failing_stash_reader));
+}
+
+TEST(TransferListTest, Parse) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "2", // total blocks
+ "1", // max stashed entries
+ "1", // max stashed blocks
+ "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1 2,0,1",
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_TRUE(static_cast<bool>(transfer_list));
+ ASSERT_EQ(4, transfer_list.version());
+ ASSERT_EQ(2, transfer_list.total_blocks());
+ ASSERT_EQ(1, transfer_list.stash_max_entries());
+ ASSERT_EQ(1, transfer_list.stash_max_blocks());
+ ASSERT_EQ(2U, transfer_list.commands().size());
+ ASSERT_EQ(Command::Type::STASH, transfer_list.commands()[0].type());
+ ASSERT_EQ(Command::Type::MOVE, transfer_list.commands()[1].type());
+}
+
+TEST(TransferListTest, Parse_InvalidCommand) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "2", // total blocks
+ "1", // max stashed entries
+ "1", // max stashed blocks
+ "stash 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1",
+ "move 1d74d1a60332fd38cf9405f1bae67917888da6cb 2,0,1 1",
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_FALSE(static_cast<bool>(transfer_list));
+}
+
+TEST(TransferListTest, Parse_ZeroTotalBlocks) {
+ std::vector<std::string> input_lines{
+ "4", // version
+ "0", // total blocks
+ "0", // max stashed entries
+ "0", // max stashed blocks
+ };
+
+ std::string err;
+ TransferList transfer_list = TransferList::Parse(android::base::Join(input_lines, '\n'), &err);
+ ASSERT_TRUE(static_cast<bool>(transfer_list));
+ ASSERT_EQ(4, transfer_list.version());
+ ASSERT_EQ(0, transfer_list.total_blocks());
+ ASSERT_EQ(0, transfer_list.stash_max_entries());
+ ASSERT_EQ(0, transfer_list.stash_max_blocks());
+ ASSERT_TRUE(transfer_list.commands().empty());
+}
diff --git a/tests/unit/dirutil_test.cpp b/tests/unit/dirutil_test.cpp
index 7f85d13..1ca786c 100644
--- a/tests/unit/dirutil_test.cpp
+++ b/tests/unit/dirutil_test.cpp
@@ -22,7 +22,8 @@
#include <android-base/test_utils.h>
#include <gtest/gtest.h>
-#include <otautil/DirUtil.h>
+
+#include "otautil/dirutil.h"
TEST(DirUtilTest, create_invalid) {
// Requesting to create an empty dir is invalid.
diff --git a/tests/unit/parse_install_logs_test.cpp b/tests/unit/parse_install_logs_test.cpp
new file mode 100644
index 0000000..8061f3b
--- /dev/null
+++ b/tests/unit/parse_install_logs_test.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <android-base/file.h>
+#include <android-base/strings.h>
+#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "otautil/parse_install_logs.h"
+
+TEST(ParseInstallLogsTest, EmptyFile) {
+ TemporaryFile last_install;
+
+ auto metrics = ParseLastInstall(last_install.path);
+ ASSERT_TRUE(metrics.empty());
+}
+
+TEST(ParseInstallLogsTest, SideloadSmoke) {
+ TemporaryFile last_install;
+ ASSERT_TRUE(android::base::WriteStringToFile("/cache/recovery/ota.zip\n0\n", last_install.path));
+ auto metrics = ParseLastInstall(last_install.path);
+ ASSERT_EQ(metrics.end(), metrics.find("ota_sideload"));
+
+ ASSERT_TRUE(android::base::WriteStringToFile("/sideload/package.zip\n0\n", last_install.path));
+ metrics = ParseLastInstall(last_install.path);
+ ASSERT_NE(metrics.end(), metrics.find("ota_sideload"));
+}
+
+TEST(ParseInstallLogsTest, ParseRecoveryUpdateMetrics) {
+ std::vector<std::string> lines = {
+ "/sideload/package.zip",
+ "0",
+ "time_total: 300",
+ "uncrypt_time: 40",
+ "source_build: 4973410",
+ "bytes_written_system: " + std::to_string(1200 * 1024 * 1024),
+ "bytes_stashed_system: " + std::to_string(300 * 1024 * 1024),
+ "bytes_written_vendor: " + std::to_string(40 * 1024 * 1024),
+ "bytes_stashed_vendor: " + std::to_string(50 * 1024 * 1024),
+ "temperature_start: 37000",
+ "temperature_end: 38000",
+ "temperature_max: 39000",
+ "error: 22",
+ "cause: 55",
+ };
+
+ auto metrics = ParseRecoveryUpdateMetrics(lines);
+
+ std::map<std::string, int64_t> expected_result = {
+ { "ota_time_total", 300 }, { "ota_uncrypt_time", 40 },
+ { "ota_source_version", 4973410 }, { "ota_written_in_MiBs", 1240 },
+ { "ota_stashed_in_MiBs", 350 }, { "ota_temperature_start", 37000 },
+ { "ota_temperature_end", 38000 }, { "ota_temperature_max", 39000 },
+ { "ota_non_ab_error_code", 22 }, { "ota_non_ab_cause_code", 55 },
+ };
+
+ ASSERT_EQ(expected_result, metrics);
+}
diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp
index 7ae193e..fc72f2f 100644
--- a/tests/unit/rangeset_test.cpp
+++ b/tests/unit/rangeset_test.cpp
@@ -209,6 +209,7 @@
ASSERT_EQ(static_cast<size_t>(6), rs.GetBlockNumber(5));
ASSERT_EQ(static_cast<size_t>(9), rs.GetBlockNumber(8));
+ ::testing::FLAGS_gtest_death_test_style = "threadsafe";
// Out of bound.
ASSERT_EXIT(rs.GetBlockNumber(9), ::testing::KilledBySignal(SIGABRT), "");
}
@@ -284,6 +285,8 @@
ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106));
ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10));
+
+ ::testing::FLAGS_gtest_death_test_style = "threadsafe";
// block#10 not in range.
ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), "");
}
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
new file mode 100644
index 0000000..7d97a00
--- /dev/null
+++ b/tests/unit/screen_ui_test.cpp
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <stddef.h>
+#include <stdio.h>
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/stringprintf.h>
+#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
+
+#include "common/test_constants.h"
+#include "device.h"
+#include "minui/minui.h"
+#include "otautil/paths.h"
+#include "private/resources.h"
+#include "screen_ui.h"
+
+static const std::vector<std::string> HEADERS{ "header" };
+static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" };
+
+TEST(ScreenUITest, StartPhoneMenuSmoke) {
+ Menu menu(false, 10, 20, HEADERS, ITEMS, 0);
+ ASSERT_FALSE(menu.scrollable());
+ ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
+ ASSERT_EQ(5u, menu.ItemsCount());
+
+ std::string message;
+ ASSERT_FALSE(menu.ItemsOverflow(&message));
+ for (size_t i = 0; i < menu.ItemsCount(); i++) {
+ ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+ }
+
+ ASSERT_EQ(0, menu.selection());
+}
+
+TEST(ScreenUITest, StartWearMenuSmoke) {
+ Menu menu(true, 10, 8, HEADERS, ITEMS, 1);
+ ASSERT_TRUE(menu.scrollable());
+ ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
+ ASSERT_EQ(5u, menu.ItemsCount());
+
+ std::string message;
+ ASSERT_FALSE(menu.ItemsOverflow(&message));
+ for (size_t i = 0; i < menu.ItemsCount() - 1; i++) {
+ ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+ }
+ // Test of the last item is truncated
+ ASSERT_EQ("12345678", menu.TextItem(4));
+ ASSERT_EQ(1, menu.selection());
+}
+
+TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
+ Menu menu(false, 1, 20, HEADERS, ITEMS, 0);
+ ASSERT_FALSE(menu.scrollable());
+ ASSERT_EQ(1u, menu.ItemsCount());
+
+ std::string message;
+ ASSERT_FALSE(menu.ItemsOverflow(&message));
+ for (size_t i = 0; i < menu.ItemsCount(); i++) {
+ ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+ }
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(1u, menu.MenuEnd());
+}
+
+TEST(ScreenUITest, StartWearMenuItemsOverflow) {
+ Menu menu(true, 1, 20, HEADERS, ITEMS, 0);
+ ASSERT_TRUE(menu.scrollable());
+ ASSERT_EQ(5u, menu.ItemsCount());
+
+ std::string message;
+ ASSERT_TRUE(menu.ItemsOverflow(&message));
+ ASSERT_EQ("Current item: 1/5", message);
+
+ for (size_t i = 0; i < menu.ItemsCount(); i++) {
+ ASSERT_EQ(ITEMS[i], menu.TextItem(i));
+ }
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(1u, menu.MenuEnd());
+}
+
+TEST(ScreenUITest, PhoneMenuSelectSmoke) {
+ int sel = 0;
+ Menu menu(false, 10, 20, HEADERS, ITEMS, sel);
+ // Mimic down button 10 times (2 * items size)
+ for (int i = 0; i < 10; i++) {
+ sel = menu.Select(++sel);
+ ASSERT_EQ(sel, menu.selection());
+
+ // Wraps the selection for unscrollable menu when it reaches the boundary.
+ int expected = (i + 1) % 5;
+ ASSERT_EQ(expected, menu.selection());
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(5u, menu.MenuEnd());
+ }
+
+ // Mimic up button 10 times
+ for (int i = 0; i < 10; i++) {
+ sel = menu.Select(--sel);
+ ASSERT_EQ(sel, menu.selection());
+
+ int expected = (9 - i) % 5;
+ ASSERT_EQ(expected, menu.selection());
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(5u, menu.MenuEnd());
+ }
+}
+
+TEST(ScreenUITest, WearMenuSelectSmoke) {
+ int sel = 0;
+ Menu menu(true, 10, 20, HEADERS, ITEMS, sel);
+ // Mimic pressing down button 10 times (2 * items size)
+ for (int i = 0; i < 10; i++) {
+ sel = menu.Select(++sel);
+ ASSERT_EQ(sel, menu.selection());
+
+ // Stops the selection at the boundary if the menu is scrollable.
+ int expected = std::min(i + 1, 4);
+ ASSERT_EQ(expected, menu.selection());
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(5u, menu.MenuEnd());
+ }
+
+ // Mimic pressing up button 10 times
+ for (int i = 0; i < 10; i++) {
+ sel = menu.Select(--sel);
+ ASSERT_EQ(sel, menu.selection());
+
+ int expected = std::max(3 - i, 0);
+ ASSERT_EQ(expected, menu.selection());
+
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(5u, menu.MenuEnd());
+ }
+}
+
+TEST(ScreenUITest, WearMenuSelectItemsOverflow) {
+ int sel = 1;
+ Menu menu(true, 3, 20, HEADERS, ITEMS, sel);
+ ASSERT_EQ(5u, menu.ItemsCount());
+
+ // Scroll the menu to the end, and check the start & end of menu.
+ for (int i = 0; i < 3; i++) {
+ sel = menu.Select(++sel);
+ ASSERT_EQ(i + 2, sel);
+ ASSERT_EQ(static_cast<size_t>(i), menu.MenuStart());
+ ASSERT_EQ(static_cast<size_t>(i + 3), menu.MenuEnd());
+ }
+
+ // Press down button one more time won't change the MenuStart() and MenuEnd().
+ sel = menu.Select(++sel);
+ ASSERT_EQ(4, sel);
+ ASSERT_EQ(2u, menu.MenuStart());
+ ASSERT_EQ(5u, menu.MenuEnd());
+
+ // Scroll the menu to the top.
+ // The expected menu sel, start & ends are:
+ // sel 3, start 2, end 5
+ // sel 2, start 2, end 5
+ // sel 1, start 1, end 4
+ // sel 0, start 0, end 3
+ for (int i = 0; i < 4; i++) {
+ sel = menu.Select(--sel);
+ ASSERT_EQ(3 - i, sel);
+ ASSERT_EQ(static_cast<size_t>(std::min(3 - i, 2)), menu.MenuStart());
+ ASSERT_EQ(static_cast<size_t>(std::min(6 - i, 5)), menu.MenuEnd());
+ }
+
+ // Press up button one more time won't change the MenuStart() and MenuEnd().
+ sel = menu.Select(--sel);
+ ASSERT_EQ(0, sel);
+ ASSERT_EQ(0u, menu.MenuStart());
+ ASSERT_EQ(3u, menu.MenuEnd());
+}
+
+static constexpr int kMagicAction = 101;
+
+enum class KeyCode : int {
+ TIMEOUT = -1,
+ NO_OP = 0,
+ UP = 1,
+ DOWN = 2,
+ ENTER = 3,
+ MAGIC = 1001,
+ LAST,
+};
+
+static const std::map<KeyCode, int> kKeyMapping{
+ // clang-format off
+ { KeyCode::NO_OP, Device::kNoAction },
+ { KeyCode::UP, Device::kHighlightUp },
+ { KeyCode::DOWN, Device::kHighlightDown },
+ { KeyCode::ENTER, Device::kInvokeItem },
+ { KeyCode::MAGIC, kMagicAction },
+ // clang-format on
+};
+
+class TestableScreenRecoveryUI : public ScreenRecoveryUI {
+ public:
+ int WaitKey() override;
+
+ void SetKeyBuffer(const std::vector<KeyCode>& buffer);
+
+ int KeyHandler(int key, bool visible) const;
+
+ // 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:
+ std::vector<KeyCode> key_buffer_;
+ size_t key_buffer_index_;
+};
+
+void TestableScreenRecoveryUI::SetKeyBuffer(const std::vector<KeyCode>& buffer) {
+ key_buffer_ = buffer;
+ key_buffer_index_ = 0;
+}
+
+int TestableScreenRecoveryUI::KeyHandler(int key, bool) const {
+ KeyCode key_code = static_cast<KeyCode>(key);
+ if (kKeyMapping.find(key_code) != kKeyMapping.end()) {
+ return kKeyMapping.at(key_code);
+ }
+ return Device::kNoAction;
+}
+
+int TestableScreenRecoveryUI::WaitKey() {
+ if (IsKeyInterrupted()) {
+ return static_cast<int>(RecoveryUI::KeyError::INTERRUPTED);
+ }
+
+ CHECK_LT(key_buffer_index_, key_buffer_.size());
+ return static_cast<int>(key_buffer_[key_buffer_index_++]);
+}
+
+class ScreenRecoveryUITest : public ::testing::Test {
+ protected:
+ const std::string kTestLocale = "en-US";
+ const std::string kTestRtlLocale = "ar";
+ const std::string kTestRtlLocaleWithSuffix = "ar-EG";
+
+ void SetUp() override {
+ has_graphics_ = gr_init() == 0;
+ gr_exit();
+
+ if (has_graphics_) {
+ ui_ = std::make_unique<TestableScreenRecoveryUI>();
+ }
+
+ testdata_dir_ = from_testdata_base("");
+ Paths::Get().set_resource_dir(testdata_dir_);
+ res_set_resource_dir(testdata_dir_);
+ }
+
+ bool has_graphics_;
+ std::unique_ptr<TestableScreenRecoveryUI> ui_;
+ std::string testdata_dir_;
+};
+
+#define RETURN_IF_NO_GRAPHICS \
+ do { \
+ if (!has_graphics_) { \
+ GTEST_LOG_(INFO) << "Test skipped due to no available graphics device"; \
+ return; \
+ } \
+ } while (false)
+
+TEST_F(ScreenRecoveryUITest, Init) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ASSERT_EQ(kTestLocale, ui_->GetLocale());
+ ASSERT_FALSE(ui_->GetRtlLocale());
+ ASSERT_FALSE(ui_->IsTextVisible());
+ ASSERT_FALSE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, dtor_NotCallingInit) {
+ ui_.reset();
+ ASSERT_FALSE(ui_);
+}
+
+TEST_F(ScreenRecoveryUITest, ShowText) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ASSERT_FALSE(ui_->IsTextVisible());
+ ui_->ShowText(true);
+ ASSERT_TRUE(ui_->IsTextVisible());
+ ASSERT_TRUE(ui_->WasTextEverVisible());
+
+ ui_->ShowText(false);
+ ASSERT_FALSE(ui_->IsTextVisible());
+ ASSERT_TRUE(ui_->WasTextEverVisible());
+}
+
+TEST_F(ScreenRecoveryUITest, RtlLocale) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestRtlLocale));
+ ASSERT_TRUE(ui_->GetRtlLocale());
+}
+
+TEST_F(ScreenRecoveryUITest, RtlLocaleWithSuffix) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestRtlLocaleWithSuffix));
+ ASSERT_TRUE(ui_->GetRtlLocale());
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ui_->SetKeyBuffer({
+ KeyCode::UP,
+ KeyCode::DOWN,
+ KeyCode::UP,
+ KeyCode::DOWN,
+ KeyCode::ENTER,
+ });
+ ASSERT_EQ(3u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+
+ ui_->SetKeyBuffer({
+ KeyCode::UP,
+ KeyCode::UP,
+ KeyCode::NO_OP,
+ KeyCode::NO_OP,
+ KeyCode::UP,
+ KeyCode::ENTER,
+ });
+ ASSERT_EQ(2u, ui_->ShowMenu(HEADERS, ITEMS, 0, true,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_NotMenuOnly) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ui_->SetKeyBuffer({
+ KeyCode::MAGIC,
+ });
+ ASSERT_EQ(static_cast<size_t>(kMagicAction),
+ ui_->ShowMenu(HEADERS, ITEMS, 3, false,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ui_->SetKeyBuffer({
+ KeyCode::TIMEOUT,
+ });
+ ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::TIMED_OUT),
+ ui_->ShowMenu(HEADERS, ITEMS, 3, true, nullptr));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenu_TimedOut_TextWasEverVisible) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ui_->ShowText(true);
+ ui_->ShowText(false);
+ ASSERT_TRUE(ui_->WasTextEverVisible());
+
+ ui_->SetKeyBuffer({
+ KeyCode::TIMEOUT,
+ KeyCode::DOWN,
+ KeyCode::ENTER,
+ });
+ ASSERT_EQ(4u, ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, ShowMenuWithInterrupt) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ ui_->SetKeyBuffer({
+ KeyCode::UP,
+ KeyCode::DOWN,
+ KeyCode::UP,
+ KeyCode::DOWN,
+ KeyCode::ENTER,
+ });
+
+ ui_->InterruptKey();
+ ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED),
+ ui_->ShowMenu(HEADERS, ITEMS, 3, true,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+
+ ui_->SetKeyBuffer({
+ KeyCode::UP,
+ KeyCode::UP,
+ KeyCode::NO_OP,
+ KeyCode::NO_OP,
+ KeyCode::UP,
+ KeyCode::ENTER,
+ });
+ ASSERT_EQ(static_cast<size_t>(RecoveryUI::KeyError::INTERRUPTED),
+ ui_->ShowMenu(HEADERS, ITEMS, 0, true,
+ std::bind(&TestableScreenRecoveryUI::KeyHandler, ui_.get(),
+ std::placeholders::_1, std::placeholders::_2)));
+}
+
+TEST_F(ScreenRecoveryUITest, LoadAnimation) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ // Make a few copies of loop00000.png from testdata.
+ std::string image_data;
+ ASSERT_TRUE(android::base::ReadFileToString(testdata_dir_ + "/loop00000.png", &image_data));
+
+ std::vector<std::string> tempfiles;
+ TemporaryDir resource_dir;
+ for (const auto& name : { "00002", "00100", "00050" }) {
+ tempfiles.push_back(android::base::StringPrintf("%s/loop%s.png", resource_dir.path, name));
+ ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back()));
+ }
+ for (const auto& name : { "00", "01" }) {
+ tempfiles.push_back(android::base::StringPrintf("%s/intro%s.png", resource_dir.path, name));
+ ASSERT_TRUE(android::base::WriteStringToFile(image_data, tempfiles.back()));
+ }
+ Paths::Get().set_resource_dir(resource_dir.path);
+
+ ui_->RunLoadAnimation();
+
+ ASSERT_EQ(2u, ui_->GetIntroFrames());
+ ASSERT_EQ(3u, ui_->GetLoopFrames());
+
+ for (const auto& name : tempfiles) {
+ ASSERT_EQ(0, unlink(name.c_str()));
+ }
+}
+
+TEST_F(ScreenRecoveryUITest, LoadAnimation_MissingAnimation) {
+ RETURN_IF_NO_GRAPHICS;
+
+ ASSERT_TRUE(ui_->Init(kTestLocale));
+ // We need a dir that doesn't contain any animation. However, using TemporaryDir will give
+ // leftovers since this is a death test where TemporaryDir::~TemporaryDir() won't be called.
+ Paths::Get().set_resource_dir("/proc/self");
+
+ ::testing::FLAGS_gtest_death_test_style = "threadsafe";
+ ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), "");
+}
+
+#undef RETURN_IF_NO_GRAPHICS
diff --git a/tests/unit/sysutil_test.cpp b/tests/unit/sysutil_test.cpp
index 434ee25..de8ff70 100644
--- a/tests/unit/sysutil_test.cpp
+++ b/tests/unit/sysutil_test.cpp
@@ -14,14 +14,13 @@
* limitations under the License.
*/
-#include <gtest/gtest.h>
-
#include <string>
#include <android-base/file.h>
#include <android-base/test_utils.h>
+#include <gtest/gtest.h>
-#include "otautil/SysUtil.h"
+#include "otautil/sysutil.h"
TEST(SysUtilTest, InvalidArgs) {
MemMapping mapping;
@@ -128,3 +127,13 @@
ASSERT_TRUE(android::base::WriteStringToFile("/doesntexist\n4096 4096\n1\n0 1\n", temp_file.path));
ASSERT_FALSE(mapping.MapFile(filename));
}
+
+TEST(SysUtilTest, StringVectorToNullTerminatedArray) {
+ std::vector<std::string> args{ "foo", "bar", "baz" };
+ auto args_with_nullptr = StringVectorToNullTerminatedArray(args);
+ ASSERT_EQ(4, args_with_nullptr.size());
+ ASSERT_STREQ("foo", args_with_nullptr[0]);
+ ASSERT_STREQ("bar", args_with_nullptr[1]);
+ ASSERT_STREQ("baz", args_with_nullptr[2]);
+ ASSERT_EQ(nullptr, args_with_nullptr[3]);
+}
diff --git a/tests/unit/zip_test.cpp b/tests/unit/zip_test.cpp
index 8276685..47f33d9 100644
--- a/tests/unit/zip_test.cpp
+++ b/tests/unit/zip_test.cpp
@@ -23,10 +23,10 @@
#include <android-base/file.h>
#include <android-base/test_utils.h>
#include <gtest/gtest.h>
-#include <otautil/SysUtil.h>
#include <ziparchive/zip_archive.h>
#include "common/test_constants.h"
+#include "otautil/sysutil.h"
TEST(ZipTest, OpenFromMemory) {
std::string zip_path = from_testdata_base("ziptest_dummy-update.zip");
diff --git a/tools/Android.mk b/tools/Android.mk
deleted file mode 100644
index 6571161..0000000
--- a/tools/Android.mk
+++ /dev/null
@@ -1 +0,0 @@
-include $(all-subdir-makefiles)
diff --git a/tools/dumpkey/Android.bp b/tools/dumpkey/Android.bp
new file mode 100644
index 0000000..eb45e31
--- /dev/null
+++ b/tools/dumpkey/Android.bp
@@ -0,0 +1,27 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+java_library_host {
+ name: "dumpkey",
+
+ manifest: "DumpPublicKey.mf",
+
+ srcs: [
+ "DumpPublicKey.java",
+ ],
+
+ static_libs: [
+ "bouncycastle-host",
+ ],
+}
diff --git a/tools/recovery_l10n/Android.bp b/tools/recovery_l10n/Android.bp
new file mode 100644
index 0000000..d0a6d4b
--- /dev/null
+++ b/tools/recovery_l10n/Android.bp
@@ -0,0 +1,23 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_app {
+ name: "RecoveryLocalizer",
+
+ sdk_version: "current",
+
+ srcs: [
+ "src/**/*.java",
+ ],
+}
diff --git a/tools/recovery_l10n/Android.mk b/tools/recovery_l10n/Android.mk
deleted file mode 100644
index 7197c5c..0000000
--- a/tools/recovery_l10n/Android.mk
+++ /dev/null
@@ -1,13 +0,0 @@
-# Copyright 2012 Google Inc. All Rights Reserved.
-
-LOCAL_PATH := $(call my-dir)
-
-include $(CLEAR_VARS)
-
-LOCAL_PACKAGE_NAME := RecoveryLocalizer
-LOCAL_SDK_VERSION := current
-LOCAL_MODULE_TAGS := optional
-
-LOCAL_SRC_FILES := $(call all-java-files-under, src)
-
-include $(BUILD_PACKAGE)
diff --git a/ui.cpp b/ui.cpp
index 3c9ded7..14b5b09 100644
--- a/ui.cpp
+++ b/ui.cpp
@@ -18,49 +18,52 @@
#include <errno.h>
#include <fcntl.h>
-#include <linux/input.h>
-#include <pthread.h>
-#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
-#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
+#include <chrono>
#include <functional>
#include <string>
+#include <thread>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/parseint.h>
#include <android-base/properties.h>
#include <android-base/strings.h>
-#include <cutils/android_reboot.h>
-#include <minui/minui.h>
-#include "common.h"
+#include "minui/minui.h"
+#include "otautil/sysutil.h"
#include "roots.h"
-#include "device.h"
-static constexpr int UI_WAIT_KEY_TIMEOUT_SEC = 120;
-static constexpr const char* BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/brightness";
-static constexpr const char* MAX_BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/max_brightness";
-static constexpr const char* BRIGHTNESS_FILE_SDM =
- "/sys/class/backlight/panel0-backlight/brightness";
-static constexpr const char* MAX_BRIGHTNESS_FILE_SDM =
+using namespace std::chrono_literals;
+
+constexpr int UI_WAIT_KEY_TIMEOUT_SEC = 120;
+constexpr const char* BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/brightness";
+constexpr const char* MAX_BRIGHTNESS_FILE = "/sys/class/leds/lcd-backlight/max_brightness";
+constexpr const char* BRIGHTNESS_FILE_SDM = "/sys/class/backlight/panel0-backlight/brightness";
+constexpr const char* MAX_BRIGHTNESS_FILE_SDM =
"/sys/class/backlight/panel0-backlight/max_brightness";
+constexpr int kDefaultTouchLowThreshold = 50;
+constexpr int kDefaultTouchHighThreshold = 90;
+
RecoveryUI::RecoveryUI()
: brightness_normal_(50),
brightness_dimmed_(25),
brightness_file_(BRIGHTNESS_FILE),
max_brightness_file_(MAX_BRIGHTNESS_FILE),
touch_screen_allowed_(false),
- kTouchLowThreshold(RECOVERY_UI_TOUCH_LOW_THRESHOLD),
- kTouchHighThreshold(RECOVERY_UI_TOUCH_HIGH_THRESHOLD),
+ touch_low_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_low_threshold",
+ kDefaultTouchLowThreshold)),
+ touch_high_threshold_(android::base::GetIntProperty("ro.recovery.ui.touch_high_threshold",
+ kDefaultTouchHighThreshold)),
+ key_interrupted_(false),
key_queue_len(0),
key_last_down(-1),
key_long_press(false),
@@ -75,11 +78,17 @@
touch_slot_(0),
is_bootreason_recovery_ui_(false),
screensaver_state_(ScreensaverState::DISABLED) {
- pthread_mutex_init(&key_queue_mutex, nullptr);
- pthread_cond_init(&key_queue_cond, nullptr);
memset(key_pressed, 0, sizeof(key_pressed));
}
+RecoveryUI::~RecoveryUI() {
+ ev_exit();
+ input_thread_stopped_ = true;
+ if (input_thread_.joinable()) {
+ input_thread_.join();
+ }
+}
+
void RecoveryUI::OnKeyDetected(int key_code) {
if (key_code == KEY_POWER) {
has_power_key = true;
@@ -92,16 +101,6 @@
}
}
-// Reads input events, handles special hot keys, and adds to the key queue.
-static void* InputThreadLoop(void*) {
- while (true) {
- if (!ev_wait(-1)) {
- ev_dispatch();
- }
- }
- return nullptr;
-}
-
bool RecoveryUI::InitScreensaver() {
// Disabled.
if (brightness_normal_ == 0 || brightness_dimmed_ > brightness_normal_) {
@@ -168,7 +167,15 @@
LOG(INFO) << "Screensaver disabled";
}
- pthread_create(&input_thread_, nullptr, InputThreadLoop, nullptr);
+ // Create a separate thread that handles input events.
+ input_thread_ = std::thread([this]() {
+ while (!this->input_thread_stopped_) {
+ if (!ev_wait(500)) {
+ ev_dispatch();
+ }
+ }
+ });
+
return true;
}
@@ -176,15 +183,15 @@
enum SwipeDirection { UP, DOWN, RIGHT, LEFT } direction;
// We only consider a valid swipe if:
- // - the delta along one axis is below kTouchLowThreshold;
- // - and the delta along the other axis is beyond kTouchHighThreshold.
- if (abs(dy) < kTouchLowThreshold && abs(dx) > kTouchHighThreshold) {
+ // - the delta along one axis is below touch_low_threshold_;
+ // - and the delta along the other axis is beyond touch_high_threshold_.
+ if (abs(dy) < touch_low_threshold_ && abs(dx) > touch_high_threshold_) {
direction = dx < 0 ? SwipeDirection::LEFT : SwipeDirection::RIGHT;
- } else if (abs(dx) < kTouchLowThreshold && abs(dy) > kTouchHighThreshold) {
+ } else if (abs(dx) < touch_low_threshold_ && abs(dy) > touch_high_threshold_) {
direction = dy < 0 ? SwipeDirection::UP : SwipeDirection::DOWN;
} else {
- LOG(DEBUG) << "Ignored " << dx << " " << dy << " (low: " << kTouchLowThreshold
- << ", high: " << kTouchHighThreshold << ")";
+ LOG(DEBUG) << "Ignored " << dx << " " << dy << " (low: " << touch_low_threshold_
+ << ", high: " << touch_high_threshold_ << ")";
return;
}
@@ -325,46 +332,38 @@
return 0;
}
-// Process a key-up or -down event. A key is "registered" when it is
-// pressed and then released, with no other keypresses or releases in
-// between. Registered keys are passed to CheckKey() to see if it
-// should trigger a visibility toggle, an immediate reboot, or be
-// queued to be processed next time the foreground thread wants a key
-// (eg, for the menu).
+// Processes a key-up or -down event. A key is "registered" when it is pressed and then released,
+// with no other keypresses or releases in between. Registered keys are passed to CheckKey() to
+// see if it should trigger a visibility toggle, an immediate reboot, or be queued to be processed
+// next time the foreground thread wants a key (eg, for the menu).
//
-// We also keep track of which keys are currently down so that
-// CheckKey can call IsKeyPressed to see what other keys are held when
-// a key is registered.
+// We also keep track of which keys are currently down so that CheckKey() can call IsKeyPressed()
+// to see what other keys are held when a key is registered.
//
// updown == 1 for key down events; 0 for key up events
void RecoveryUI::ProcessKey(int key_code, int updown) {
bool register_key = false;
bool long_press = false;
- bool reboot_enabled;
- pthread_mutex_lock(&key_queue_mutex);
- key_pressed[key_code] = updown;
- if (updown) {
- ++key_down_count;
- key_last_down = key_code;
- key_long_press = false;
- key_timer_t* info = new key_timer_t;
- info->ui = this;
- info->key_code = key_code;
- info->count = key_down_count;
- pthread_t thread;
- pthread_create(&thread, nullptr, &RecoveryUI::time_key_helper, info);
- pthread_detach(thread);
- } else {
- if (key_last_down == key_code) {
- long_press = key_long_press;
- register_key = true;
+ {
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
+ key_pressed[key_code] = updown;
+ if (updown) {
+ ++key_down_count;
+ key_last_down = key_code;
+ key_long_press = false;
+ std::thread time_key_thread(&RecoveryUI::TimeKey, this, key_code, key_down_count);
+ time_key_thread.detach();
+ } else {
+ if (key_last_down == key_code) {
+ long_press = key_long_press;
+ register_key = true;
+ }
+ key_last_down = -1;
}
- key_last_down = -1;
}
- reboot_enabled = enable_reboot;
- pthread_mutex_unlock(&key_queue_mutex);
+ bool reboot_enabled = enable_reboot;
if (register_key) {
switch (CheckKey(key_code, long_press)) {
case RecoveryUI::IGNORE:
@@ -390,67 +389,87 @@
}
}
-void* RecoveryUI::time_key_helper(void* cookie) {
- key_timer_t* info = static_cast<key_timer_t*>(cookie);
- info->ui->time_key(info->key_code, info->count);
- delete info;
- return nullptr;
-}
-
-void RecoveryUI::time_key(int key_code, int count) {
- usleep(750000); // 750 ms == "long"
+void RecoveryUI::TimeKey(int key_code, int count) {
+ std::this_thread::sleep_for(750ms); // 750 ms == "long"
bool long_press = false;
- pthread_mutex_lock(&key_queue_mutex);
- if (key_last_down == key_code && key_down_count == count) {
- long_press = key_long_press = true;
+ {
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
+ if (key_last_down == key_code && key_down_count == count) {
+ long_press = key_long_press = true;
+ }
}
- pthread_mutex_unlock(&key_queue_mutex);
if (long_press) KeyLongPress(key_code);
}
void RecoveryUI::EnqueueKey(int key_code) {
- pthread_mutex_lock(&key_queue_mutex);
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
const int queue_max = sizeof(key_queue) / sizeof(key_queue[0]);
if (key_queue_len < queue_max) {
key_queue[key_queue_len++] = key_code;
- pthread_cond_signal(&key_queue_cond);
+ key_queue_cond.notify_one();
}
- pthread_mutex_unlock(&key_queue_mutex);
+}
+
+void RecoveryUI::SetScreensaverState(ScreensaverState state) {
+ switch (state) {
+ case ScreensaverState::NORMAL:
+ if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
+ brightness_file_)) {
+ screensaver_state_ = ScreensaverState::NORMAL;
+ LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
+ << "%)";
+ } else {
+ LOG(ERROR) << "Unable to set brightness to normal";
+ }
+ break;
+ case ScreensaverState::DIMMED:
+ if (android::base::WriteStringToFile(std::to_string(brightness_dimmed_value_),
+ brightness_file_)) {
+ LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
+ << "%)";
+ screensaver_state_ = ScreensaverState::DIMMED;
+ } else {
+ LOG(ERROR) << "Unable to set brightness to dim";
+ }
+ break;
+ case ScreensaverState::OFF:
+ if (android::base::WriteStringToFile("0", brightness_file_)) {
+ LOG(INFO) << "Brightness: 0 (off)";
+ screensaver_state_ = ScreensaverState::OFF;
+ } else {
+ LOG(ERROR) << "Unable to set brightness to off";
+ }
+ break;
+ default:
+ LOG(ERROR) << "Invalid screensaver state";
+ }
}
int RecoveryUI::WaitKey() {
- pthread_mutex_lock(&key_queue_mutex);
+ std::unique_lock<std::mutex> lk(key_queue_mutex);
- // Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is
- // plugged in.
+ // Check for a saved key queue interruption.
+ if (key_interrupted_) {
+ SetScreensaverState(ScreensaverState::NORMAL);
+ return static_cast<int>(KeyError::INTERRUPTED);
+ }
+
+ // Time out after UI_WAIT_KEY_TIMEOUT_SEC, unless a USB cable is plugged in.
do {
- struct timeval now;
- struct timespec timeout;
- gettimeofday(&now, nullptr);
- timeout.tv_sec = now.tv_sec;
- timeout.tv_nsec = now.tv_usec * 1000;
- timeout.tv_sec += UI_WAIT_KEY_TIMEOUT_SEC;
-
- int rc = 0;
- while (key_queue_len == 0 && rc != ETIMEDOUT) {
- rc = pthread_cond_timedwait(&key_queue_cond, &key_queue_mutex, &timeout);
+ bool rc = key_queue_cond.wait_for(lk, std::chrono::seconds(UI_WAIT_KEY_TIMEOUT_SEC), [this] {
+ return this->key_queue_len != 0 || key_interrupted_;
+ });
+ if (key_interrupted_) {
+ SetScreensaverState(ScreensaverState::NORMAL);
+ return static_cast<int>(KeyError::INTERRUPTED);
}
-
if (screensaver_state_ != ScreensaverState::DISABLED) {
- if (rc == ETIMEDOUT) {
- // Lower the brightness level: NORMAL -> DIMMED; DIMMED -> OFF.
+ if (!rc) {
+ // Must be after a timeout. Lower the brightness level: NORMAL -> DIMMED; DIMMED -> OFF.
if (screensaver_state_ == ScreensaverState::NORMAL) {
- if (android::base::WriteStringToFile(std::to_string(brightness_dimmed_value_),
- brightness_file_)) {
- LOG(INFO) << "Brightness: " << brightness_dimmed_value_ << " (" << brightness_dimmed_
- << "%)";
- screensaver_state_ = ScreensaverState::DIMMED;
- }
+ SetScreensaverState(ScreensaverState::DIMMED);
} else if (screensaver_state_ == ScreensaverState::DIMMED) {
- if (android::base::WriteStringToFile("0", brightness_file_)) {
- LOG(INFO) << "Brightness: 0 (off)";
- screensaver_state_ = ScreensaverState::OFF;
- }
+ SetScreensaverState(ScreensaverState::OFF);
}
} else if (screensaver_state_ != ScreensaverState::NORMAL) {
// Drop the first key if it's changing from OFF to NORMAL.
@@ -461,25 +480,27 @@
}
// Reset the brightness to normal.
- if (android::base::WriteStringToFile(std::to_string(brightness_normal_value_),
- brightness_file_)) {
- screensaver_state_ = ScreensaverState::NORMAL;
- LOG(INFO) << "Brightness: " << brightness_normal_value_ << " (" << brightness_normal_
- << "%)";
- }
+ SetScreensaverState(ScreensaverState::NORMAL);
}
}
} while (IsUsbConnected() && key_queue_len == 0);
- int key = -1;
+ int key = static_cast<int>(KeyError::TIMED_OUT);
if (key_queue_len > 0) {
key = key_queue[0];
memcpy(&key_queue[0], &key_queue[1], sizeof(int) * --key_queue_len);
}
- pthread_mutex_unlock(&key_queue_mutex);
return key;
}
+void RecoveryUI::InterruptKey() {
+ {
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
+ key_interrupted_ = true;
+ }
+ key_queue_cond.notify_one();
+}
+
bool RecoveryUI::IsUsbConnected() {
int fd = open("/sys/class/android_usb/android0/state", O_RDONLY);
if (fd < 0) {
@@ -497,16 +518,14 @@
}
bool RecoveryUI::IsKeyPressed(int key) {
- pthread_mutex_lock(&key_queue_mutex);
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
int pressed = key_pressed[key];
- pthread_mutex_unlock(&key_queue_mutex);
return pressed;
}
bool RecoveryUI::IsLongPress() {
- pthread_mutex_lock(&key_queue_mutex);
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
bool result = key_long_press;
- pthread_mutex_unlock(&key_queue_mutex);
return result;
}
@@ -523,15 +542,15 @@
}
void RecoveryUI::FlushKeys() {
- pthread_mutex_lock(&key_queue_mutex);
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
key_queue_len = 0;
- pthread_mutex_unlock(&key_queue_mutex);
}
RecoveryUI::KeyAction RecoveryUI::CheckKey(int key, bool is_long_press) {
- pthread_mutex_lock(&key_queue_mutex);
- key_long_press = false;
- pthread_mutex_unlock(&key_queue_mutex);
+ {
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
+ key_long_press = false;
+ }
// If we have power and volume up keys, that chord is the signal to toggle the text display.
if (HasThreeButtons() || (HasPowerKey() && HasTouchScreen() && touch_screen_allowed_)) {
@@ -554,9 +573,7 @@
// Press power seven times in a row to reboot.
if (key == KEY_POWER) {
- pthread_mutex_lock(&key_queue_mutex);
bool reboot_enabled = enable_reboot;
- pthread_mutex_unlock(&key_queue_mutex);
if (reboot_enabled) {
++consecutive_power_keys;
@@ -576,7 +593,6 @@
}
void RecoveryUI::SetEnableReboot(bool enabled) {
- pthread_mutex_lock(&key_queue_mutex);
+ std::lock_guard<std::mutex> lg(key_queue_mutex);
enable_reboot = enabled;
- pthread_mutex_unlock(&key_queue_mutex);
}
diff --git a/ui.h b/ui.h
index 4c54d69..e0fb13e 100644
--- a/ui.h
+++ b/ui.h
@@ -17,11 +17,15 @@
#ifndef RECOVERY_UI_H
#define RECOVERY_UI_H
-#include <linux/input.h>
-#include <pthread.h>
-#include <time.h>
+#include <linux/input.h> // KEY_MAX
+#include <atomic>
+#include <condition_variable>
+#include <functional>
+#include <mutex>
#include <string>
+#include <thread>
+#include <vector>
// Abstract class for controlling the user interface during recovery.
class RecoveryUI {
@@ -47,14 +51,21 @@
IGNORE
};
+ enum class KeyError : int {
+ TIMED_OUT = -1,
+ INTERRUPTED = -2,
+ };
+
RecoveryUI();
- virtual ~RecoveryUI() {}
+ virtual ~RecoveryUI();
// Initializes the object; called before anything else. UI texts will be initialized according to
// the given locale. Returns true on success.
virtual bool Init(const std::string& locale);
+ virtual std::string GetLocale() const = 0;
+
// Shows a stage indicator. Called immediately after Init().
virtual void SetStage(int current, int max) = 0;
@@ -87,13 +98,19 @@
virtual void Print(const char* fmt, ...) __printflike(2, 3) = 0;
virtual void PrintOnScreenOnly(const char* fmt, ...) __printflike(2, 3) = 0;
- virtual void ShowFile(const char* filename) = 0;
+ // Shows the contents of the given file. Caller ensures the patition that contains the file has
+ // been mounted.
+ virtual void ShowFile(const std::string& filename) = 0;
// --- key handling ---
- // Waits for a key and return it. May return -1 after timeout.
+ // Waits for a key and return it. May return TIMED_OUT after timeout and
+ // KeyError::INTERRUPTED on a key interrupt.
virtual int WaitKey();
+ // Wakes up the UI if it is waiting on key input, causing WaitKey to return KeyError::INTERRUPTED.
+ virtual void InterruptKey();
+
virtual bool IsKeyPressed(int key);
virtual bool IsLongPress();
@@ -128,17 +145,32 @@
// --- menu display ---
- // Display some header text followed by a menu of items, which appears at the top of the screen
- // (in place of any scrolling ui_print() output, if necessary).
- virtual void StartMenu(const char* const* headers, const char* const* items,
- int initial_selection) = 0;
+ virtual void SetTitle(const std::vector<std::string>& lines) = 0;
- // Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
- // selected.
- virtual int SelectMenu(int sel) = 0;
+ // Displays a menu with the given 'headers' and 'items'. The supplied 'key_handler' callback,
+ // which is typically bound to Device::HandleMenuKey(), should return the expected action for the
+ // given key code and menu visibility (e.g. to move the cursor or to select an item). Caller sets
+ // 'menu_only' to true to ensure only a menu item gets selected and returned. Otherwise if
+ // 'menu_only' is false, ShowMenu() will forward any non-negative value returned from the
+ // key_handler, which may be beyond the range of menu items. This could be used to trigger a
+ // device-specific action, even without that being listed in the menu. Caller needs to handle
+ // such a case accordingly (e.g. by calling Device::InvokeMenuItem() to process the action).
+ // Returns a non-negative value (the chosen item number or device-specific action code), or
+ // static_cast<size_t>(TIMED_OUT) if timed out waiting for input or
+ // static_cast<size_t>(ERR_KEY_INTERTUPT) if interrupted, such as by InterruptKey().
+ virtual size_t ShowMenu(const std::vector<std::string>& headers,
+ const std::vector<std::string>& items, size_t initial_selection,
+ bool menu_only, const std::function<int(int, bool)>& key_handler) = 0;
- // Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed.
- virtual void EndMenu() = 0;
+ // Resets the key interrupt status.
+ void ResetKeyInterruptStatus() {
+ key_interrupted_ = false;
+ }
+
+ // Returns the key interrupt status.
+ bool IsKeyInterrupted() const {
+ return key_interrupted_;
+ }
protected:
void EnqueueKey(int key_code);
@@ -162,31 +194,24 @@
OFF
};
- struct key_timer_t {
- RecoveryUI* ui;
- int key_code;
- int count;
- };
-
// The sensitivity when detecting a swipe.
- const int kTouchLowThreshold;
- const int kTouchHighThreshold;
+ const int touch_low_threshold_;
+ const int touch_high_threshold_;
void OnKeyDetected(int key_code);
void OnTouchDetected(int dx, int dy);
int OnInputEvent(int fd, uint32_t epevents);
void ProcessKey(int key_code, int updown);
+ void TimeKey(int key_code, int count);
bool IsUsbConnected();
- static void* time_key_helper(void* cookie);
- void time_key(int key_code, int count);
-
bool InitScreensaver();
-
+ void SetScreensaverState(ScreensaverState state);
// Key event input queue
- pthread_mutex_t key_queue_mutex;
- pthread_cond_t key_queue_cond;
+ std::mutex key_queue_mutex;
+ std::condition_variable key_queue_cond;
+ bool key_interrupted_;
int key_queue[256], key_queue_len;
char key_pressed[KEY_MAX + 1]; // under key_queue_mutex
int key_last_down; // under key_queue_mutex
@@ -213,7 +238,8 @@
bool touch_swiping_;
bool is_bootreason_recovery_ui_;
- pthread_t input_thread_;
+ std::thread input_thread_;
+ std::atomic<bool> input_thread_stopped_{ false };
ScreensaverState screensaver_state_;
diff --git a/uncrypt/Android.bp b/uncrypt/Android.bp
index aa56d2f..107a7f0 100644
--- a/uncrypt/Android.bp
+++ b/uncrypt/Android.bp
@@ -24,13 +24,15 @@
"-Werror",
],
- static_libs: [
- "libbootloader_message",
- "libotautil",
- "libfs_mgr",
+ shared_libs: [
"libbase",
+ "libbootloader_message",
"libcutils",
- "liblog",
+ "libfs_mgr",
+ ],
+
+ static_libs: [
+ "libotautil",
],
init_rc: [
diff --git a/uncrypt/uncrypt.cpp b/uncrypt/uncrypt.cpp
index bb43c2c..95f40c7 100644
--- a/uncrypt/uncrypt.cpp
+++ b/uncrypt/uncrypt.cpp
@@ -172,14 +172,15 @@
return fstab;
}
-static const char* find_block_device(const char* path, bool* encryptable, bool* encrypted, bool *f2fs_fs) {
+static const char* find_block_device(const char* path, bool* encryptable,
+ bool* encrypted, bool* f2fs_fs) {
// Look for a volume whose mount point is the prefix of path and
// return its block device. Set encrypted if it's currently
// encrypted.
- // ensure f2fs_fs is set to 0 first.
- if (f2fs_fs)
- *f2fs_fs = false;
+ // 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) {
@@ -196,8 +197,9 @@
*encrypted = true;
}
}
- if (f2fs_fs && strcmp(v->fs_type, "f2fs") == 0)
+ if (strcmp(v->fs_type, "f2fs") == 0) {
*f2fs_fs = true;
+ }
return v->blk_device;
}
}
@@ -313,15 +315,30 @@
}
}
-#ifndef F2FS_IOC_SET_DONTMOVE
+// F2FS-specific ioctl
+// It requires the below kernel commit merged in v4.16-rc1.
+// 1ad71a27124c ("f2fs: add an ioctl to disable GC for specific file")
+// In android-4.4,
+// 56ee1e817908 ("f2fs: updates on v4.16-rc1")
+// In android-4.9,
+// 2f17e34672a8 ("f2fs: updates on v4.16-rc1")
+// In android-4.14,
+// ce767d9a55bc ("f2fs: updates on v4.16-rc1")
+#ifndef F2FS_IOC_SET_PIN_FILE
#ifndef F2FS_IOCTL_MAGIC
#define F2FS_IOCTL_MAGIC 0xf5
#endif
-#define F2FS_IOC_SET_DONTMOVE _IO(F2FS_IOCTL_MAGIC, 13)
+#define F2FS_IOC_SET_PIN_FILE _IOW(F2FS_IOCTL_MAGIC, 13, __u32)
+#define F2FS_IOC_GET_PIN_FILE _IOW(F2FS_IOCTL_MAGIC, 14, __u32)
#endif
- if (f2fs_fs && ioctl(fd, F2FS_IOC_SET_DONTMOVE) < 0) {
- PLOG(ERROR) << "Failed to set non-movable file for f2fs: " << path << " on " << blk_dev;
- return kUncryptIoctlError;
+ if (f2fs_fs) {
+ __u32 set = 1;
+ int error = ioctl(fd, F2FS_IOC_SET_PIN_FILE, &set);
+ // Don't break the old kernels which don't support it.
+ if (error && errno != ENOTTY && errno != ENOTSUP) {
+ PLOG(ERROR) << "Failed to set pin_file for f2fs: " << path << " on " << blk_dev;
+ return kUncryptIoctlError;
+ }
}
off64_t pos = 0;
diff --git a/update_verifier/Android.bp b/update_verifier/Android.bp
new file mode 100644
index 0000000..1b84619
--- /dev/null
+++ b/update_verifier/Android.bp
@@ -0,0 +1,118 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+ name: "update_verifier_defaults",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ local_include_dirs: [
+ "include",
+ ],
+}
+
+cc_library_static {
+ name: "libupdate_verifier",
+
+ defaults: [
+ "update_verifier_defaults",
+ ],
+
+ srcs: [
+ "care_map.proto",
+ "update_verifier.cpp",
+ ],
+
+ export_include_dirs: [
+ "include",
+ ],
+
+ static_libs: [
+ "libotautil",
+ ],
+
+ shared_libs: [
+ "android.hardware.boot@1.0",
+ "libbase",
+ "libcutils",
+ ],
+
+ proto: {
+ type: "lite",
+ export_proto_headers: true,
+ },
+}
+
+cc_binary {
+ name: "update_verifier",
+
+ defaults: [
+ "update_verifier_defaults",
+ ],
+
+ srcs: [
+ "update_verifier_main.cpp",
+ ],
+
+ static_libs: [
+ "libupdate_verifier",
+ "libotautil",
+ ],
+
+ shared_libs: [
+ "android.hardware.boot@1.0",
+ "libbase",
+ "libcutils",
+ "libhardware",
+ "libhidlbase",
+ "liblog",
+ "libprotobuf-cpp-lite",
+ "libutils",
+ ],
+
+ init_rc: [
+ "update_verifier.rc",
+ ],
+}
+
+python_binary_host {
+ name: "care_map_generator",
+
+ srcs: [
+ "care_map_generator.py",
+ "care_map.proto",
+ ],
+ libs: [
+ "python-symbol",
+ // Soong won't add "libprotobuf-python" to the dependencies if
+ // filegroup contains .proto files. So add it here explicitly.
+ "libprotobuf-python",
+ ],
+ proto: {
+ canonical_path_from_root: false,
+ },
+
+ version: {
+ py2: {
+ enabled: true,
+ embedded_launcher: true,
+ },
+ py3: {
+ enabled: false,
+ embedded_launcher: false,
+ },
+ },
+}
diff --git a/update_verifier/Android.mk b/update_verifier/Android.mk
deleted file mode 100644
index 0ff8854..0000000
--- a/update_verifier/Android.mk
+++ /dev/null
@@ -1,77 +0,0 @@
-# Copyright (C) 2015 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-LOCAL_PATH := $(call my-dir)
-
-# libupdate_verifier (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
- update_verifier.cpp
-
-LOCAL_MODULE := libupdate_verifier
-
-LOCAL_STATIC_LIBRARIES := \
- libotautil
-
-LOCAL_SHARED_LIBRARIES := \
- libbase \
- libcutils \
- android.hardware.boot@1.0
-
-LOCAL_CFLAGS := -Wall -Werror
-
-LOCAL_EXPORT_C_INCLUDE_DIRS := \
- $(LOCAL_PATH)/include
-
-LOCAL_C_INCLUDES := \
- $(LOCAL_PATH)/include
-
-ifeq ($(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_SUPPORTS_VERITY),true)
-LOCAL_CFLAGS += -DPRODUCT_SUPPORTS_VERITY=1
-endif
-
-ifeq ($(BOARD_AVB_ENABLE),true)
-LOCAL_CFLAGS += -DBOARD_AVB_ENABLE=1
-endif
-
-include $(BUILD_STATIC_LIBRARY)
-
-# update_verifier (executable)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_SRC_FILES := \
- update_verifier_main.cpp
-
-LOCAL_MODULE := update_verifier
-LOCAL_STATIC_LIBRARIES := \
- libupdate_verifier \
- libotautil
-
-LOCAL_SHARED_LIBRARIES := \
- libbase \
- libcutils \
- libhardware \
- liblog \
- libutils \
- libhidlbase \
- android.hardware.boot@1.0
-
-LOCAL_CFLAGS := -Wall -Werror
-
-LOCAL_INIT_RC := update_verifier.rc
-
-include $(BUILD_EXECUTABLE)
diff --git a/mounts.h b/update_verifier/care_map.proto
similarity index 63%
copy from mounts.h
copy to update_verifier/care_map.proto
index 0de1ebd..15d3afa 100644
--- a/mounts.h
+++ b/update_verifier/care_map.proto
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2007 The Android Open Source Project
+ * Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,18 @@
* limitations under the License.
*/
-#ifndef MOUNTS_H_
-#define MOUNTS_H_
+syntax = "proto3";
-struct MountedVolume;
+package recovery_update_verifier;
+option optimize_for = LITE_RUNTIME;
-bool scan_mounted_volumes();
+message CareMap {
+ message PartitionInfo {
+ string name = 1;
+ string ranges = 2;
+ string id = 3;
+ string fingerprint = 4;
+ }
-MountedVolume* find_mounted_volume_by_mount_point(const char* mount_point);
-
-int unmount_mounted_volume(MountedVolume* volume);
-
-#endif
+ repeated PartitionInfo partitions = 1;
+}
diff --git a/update_verifier/care_map_generator.py b/update_verifier/care_map_generator.py
new file mode 100644
index 0000000..051d98d
--- /dev/null
+++ b/update_verifier/care_map_generator.py
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Parses a input care_map.txt in plain text format; converts it into the proto
+buf message; and writes the result to the output file.
+
+"""
+
+import argparse
+import logging
+import sys
+
+import care_map_pb2
+
+
+def GenerateCareMapProtoFromLegacyFormat(lines, fingerprint_enabled):
+ """Constructs a care map proto message from the lines of the input file."""
+
+ # Expected format of the legacy care_map.txt:
+ # system
+ # system's care_map ranges
+ # [system's fingerprint property id]
+ # [system's fingerprint]
+ # [vendor]
+ # [vendor's care_map ranges]
+ # [vendor's fingerprint property id]
+ # [vendor's fingerprint]
+ # ...
+
+ step = 4 if fingerprint_enabled else 2
+ assert len(lines) % step == 0, \
+ "line count must be multiple of {}: {}".format(step, len(lines))
+
+ care_map_proto = care_map_pb2.CareMap()
+ for index in range(0, len(lines), step):
+ info = care_map_proto.partitions.add()
+ info.name = lines[index]
+ info.ranges = lines[index + 1]
+ if fingerprint_enabled:
+ info.id = lines[index + 2]
+ info.fingerprint = lines[index + 3]
+ logging.info("Care map info: name %s, ranges %s, id %s, fingerprint %s",
+ info.name, info.ranges, info.id, info.fingerprint)
+
+ return care_map_proto
+
+
+def ParseProtoMessage(message, fingerprint_enabled):
+ """Parses the care_map proto message and returns its text representation.
+ Args:
+ message: Care_map in protobuf format.
+ fingerprint_enabled: Input protobuf message contains the fields 'id' and
+ 'fingerprint'.
+
+ Returns:
+ A string of the care_map information, similar to the care_map legacy
+ format.
+ """
+ care_map_proto = care_map_pb2.CareMap()
+ care_map_proto.MergeFromString(message)
+
+ info_list = []
+ for info in care_map_proto.partitions:
+ assert info.name, "partition name is required in care_map"
+ assert info.ranges, "source range is required in care_map"
+ info_list += [info.name, info.ranges]
+ if fingerprint_enabled:
+ assert info.id, "property id is required in care_map"
+ assert info.fingerprint, "fingerprint is required in care_map"
+ info_list += [info.id, info.fingerprint]
+
+ return '\n'.join(info_list)
+
+
+def main(argv):
+ parser = argparse.ArgumentParser(
+ description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument("input_care_map",
+ help="Path to the legacy care_map file (or path to"
+ " care_map in protobuf format if --parse_proto is"
+ " specified).")
+ parser.add_argument("output_file",
+ help="Path to output file to write the result.")
+ parser.add_argument("--no_fingerprint", action="store_false",
+ dest="fingerprint_enabled",
+ help="The 'id' and 'fingerprint' fields are disabled in"
+ " the caremap.")
+ parser.add_argument("--parse_proto", "-p", action="store_true",
+ help="Parses the input as proto message, and outputs"
+ " the care_map in plain text.")
+ parser.add_argument("--verbose", "-v", action="store_true")
+
+ args = parser.parse_args(argv)
+
+ logging_format = '%(filename)s %(levelname)s: %(message)s'
+ logging.basicConfig(level=logging.INFO if args.verbose else logging.WARNING,
+ format=logging_format)
+
+ with open(args.input_care_map, 'r') as input_care_map:
+ content = input_care_map.read()
+
+ if args.parse_proto:
+ result = ParseProtoMessage(content, args.fingerprint_enabled)
+ else:
+ care_map_proto = GenerateCareMapProtoFromLegacyFormat(
+ content.rstrip().splitlines(), args.fingerprint_enabled)
+ result = care_map_proto.SerializeToString()
+
+ with open(args.output_file, 'w') as output:
+ output.write(result)
+
+
+if __name__ == '__main__':
+ main(sys.argv[1:])
diff --git a/update_verifier/include/update_verifier/update_verifier.h b/update_verifier/include/update_verifier/update_verifier.h
index 16b394e..b00890e 100644
--- a/update_verifier/include/update_verifier/update_verifier.h
+++ b/update_verifier/include/update_verifier/update_verifier.h
@@ -16,9 +16,59 @@
#pragma once
+#include <functional>
+#include <map>
#include <string>
+#include <vector>
+#include "otautil/rangeset.h"
+
+// The update verifier performs verification upon the first boot to a new slot on A/B devices.
+// During the verification, it reads all the blocks in the care_map. And if a failure happens,
+// it rejects the current boot and triggers a fallback.
+
+// Note that update_verifier should be backward compatible to not reject care_map.txt from old
+// releases, which could otherwise fail to boot into the new release. For example, we've changed
+// the care_map format between N and O. An O update_verifier would fail to work with N care_map.txt.
+// This could be a result of sideloading an O OTA while the device having a pending N update.
int update_verifier(int argc, char** argv);
-// Exposed for testing purpose.
-bool verify_image(const std::string& care_map_name);
+// The UpdateVerifier parses the content in the care map, and continues to verify the
+// partitions by reading the cared blocks if there's no format error in the file. Otherwise,
+// it should skip the verification to avoid bricking the device.
+class UpdateVerifier {
+ public:
+ UpdateVerifier();
+
+ // This function tries to process the care_map.pb as protobuf message; and falls back to use
+ // care_map.txt if the pb format file doesn't exist. If the parsing succeeds, put the result
+ // of the pair <partition_name, ranges> into the |partition_map_|.
+ bool ParseCareMap();
+
+ // Verifies the new boot by reading all the cared blocks for partitions in |partition_map_|.
+ bool VerifyPartitions();
+
+ private:
+ friend class UpdateVerifierTest;
+ // Parses the legacy care_map.txt in plain text format.
+ bool ParseCareMapPlainText(const std::string& content);
+
+ // Finds all the dm-enabled partitions, and returns a map of <partition_name, block_device>.
+ std::map<std::string, std::string> FindDmPartitions();
+
+ // Returns true if we successfully read the blocks in |ranges| of the |dm_block_device|.
+ bool ReadBlocks(const std::string partition_name, const std::string& dm_block_device,
+ const RangeSet& ranges);
+
+ // Functions to override the care_map_prefix_ and property_reader_, used in test only.
+ void set_care_map_prefix(const std::string& prefix);
+ void set_property_reader(const std::function<std::string(const std::string&)>& property_reader);
+
+ std::map<std::string, RangeSet> partition_map_;
+ // The path to the care_map excluding the filename extension; default value:
+ // "/data/ota_package/care_map"
+ std::string care_map_prefix_;
+
+ // The function to read the device property; default value: android::base::GetProperty()
+ std::function<std::string(const std::string&)> property_reader_;
+};
diff --git a/update_verifier/update_verifier.cpp b/update_verifier/update_verifier.cpp
index 92d9313..d7cd061 100644
--- a/update_verifier/update_verifier.cpp
+++ b/update_verifier/update_verifier.cpp
@@ -15,24 +15,26 @@
*/
/*
- * This program verifies the integrity of the partitions after an A/B OTA
- * update. It gets invoked by init, and will only perform the verification if
- * it's the first boot post an A/B OTA update.
+ * update_verifier verifies the integrity of the partitions after an A/B OTA update. It gets invoked
+ * by init, and will only perform the verification if it's the first boot post an A/B OTA update
+ * (https://source.android.com/devices/tech/ota/ab/#after_reboot).
*
- * Update_verifier relies on dm-verity to capture any corruption on the partitions
- * being verified. And its behavior varies depending on the dm-verity mode.
- * Upon detection of failures:
+ * update_verifier relies on device-mapper-verity (dm-verity) to capture any corruption on the
+ * partitions being verified (https://source.android.com/security/verifiedboot). The verification
+ * will be skipped, if dm-verity is not enabled on the device.
+ *
+ * Upon detecting verification failures, the device will be rebooted, although the trigger of the
+ * reboot depends on the dm-verity mode.
* enforcing mode: dm-verity reboots the device
* eio mode: dm-verity fails the read and update_verifier reboots the device
* other mode: not supported and update_verifier reboots the device
*
- * After a predefined number of failing boot attempts, the bootloader should
- * mark the slot as unbootable and stops trying. Other dm-verity modes (
- * for example, veritymode=EIO) are not accepted and simply lead to a
- * verification failure.
+ * All these reboots prevent the device from booting into a known corrupt state. If the device
+ * continuously fails to boot into the new slot, the bootloader should mark the slot as unbootable
+ * and trigger a fallback to the old slot.
*
- * The current slot will be marked as having booted successfully if the
- * verifier reaches the end after the verification.
+ * The current slot will be marked as having booted successfully if the verifier reaches the end
+ * after the verification.
*/
#include "update_verifier/update_verifier.h"
@@ -46,8 +48,6 @@
#include <algorithm>
#include <future>
-#include <string>
-#include <vector>
#include <android-base/file.h>
#include <android-base/logging.h>
@@ -58,13 +58,15 @@
#include <android/hardware/boot/1.0/IBootControl.h>
#include <cutils/android_reboot.h>
-#include "otautil/rangeset.h"
+#include "care_map.pb.h"
using android::sp;
using android::hardware::boot::V1_0::IBootControl;
using android::hardware::boot::V1_0::BoolResult;
using android::hardware::boot::V1_0::CommandResult;
+constexpr const char* kDefaultCareMapPrefix = "/data/ota_package/care_map";
+
// Find directories in format of "/sys/block/dm-X".
static int dm_name_filter(const dirent* de) {
if (android::base::StartsWith(de->d_name, "dm-")) {
@@ -73,29 +75,29 @@
return 0;
}
-static bool read_blocks(const std::string& partition, const std::string& range_str) {
- if (partition != "system" && partition != "vendor" && partition != "product") {
- LOG(ERROR) << "Invalid partition name \"" << partition << "\"";
- return false;
- }
- // Iterate the content of "/sys/block/dm-X/dm/name". If it matches one of "system", "vendor" or
- // "product", then dm-X is a dm-wrapped device for that target. We will later read all the
- // ("cared") blocks from "/dev/block/dm-X" to ensure the target partition's integrity.
+UpdateVerifier::UpdateVerifier()
+ : care_map_prefix_(kDefaultCareMapPrefix),
+ property_reader_([](const std::string& id) { return android::base::GetProperty(id, ""); }) {}
+
+// Iterate the content of "/sys/block/dm-X/dm/name" and find all the dm-wrapped block devices.
+// We will later read all the ("cared") blocks from "/dev/block/dm-X" to ensure the target
+// partition's integrity.
+std::map<std::string, std::string> UpdateVerifier::FindDmPartitions() {
static constexpr auto DM_PATH_PREFIX = "/sys/block/";
dirent** namelist;
int n = scandir(DM_PATH_PREFIX, &namelist, dm_name_filter, alphasort);
if (n == -1) {
PLOG(ERROR) << "Failed to scan dir " << DM_PATH_PREFIX;
- return false;
+ return {};
}
if (n == 0) {
- LOG(ERROR) << "dm block device not found for " << partition;
- return false;
+ LOG(ERROR) << "No dm block device found.";
+ return {};
}
static constexpr auto DM_PATH_SUFFIX = "/dm/name";
static constexpr auto DEV_PATH = "/dev/block/";
- std::string dm_block_device;
+ std::map<std::string, std::string> dm_block_devices;
while (n--) {
std::string path = DM_PATH_PREFIX + std::string(namelist[n]->d_name) + DM_PATH_SUFFIX;
std::string content;
@@ -103,39 +105,22 @@
PLOG(WARNING) << "Failed to read " << path;
} else {
std::string dm_block_name = android::base::Trim(content);
-#ifdef BOARD_AVB_ENABLE
// AVB is using 'vroot' for the root block device but we're expecting 'system'.
if (dm_block_name == "vroot") {
dm_block_name = "system";
}
-#endif
- if (dm_block_name == partition) {
- dm_block_device = DEV_PATH + std::string(namelist[n]->d_name);
- while (n--) {
- free(namelist[n]);
- }
- break;
- }
+
+ dm_block_devices.emplace(dm_block_name, DEV_PATH + std::string(namelist[n]->d_name));
}
free(namelist[n]);
}
free(namelist);
- if (dm_block_device.empty()) {
- LOG(ERROR) << "Failed to find dm block device for " << partition;
- return false;
- }
+ return dm_block_devices;
+}
- // For block range string, first integer 'count' equals 2 * total number of valid ranges,
- // followed by 'count' number comma separated integers. Every two integers reprensent a
- // block range with the first number included in range but second number not included.
- // For example '4,64536,65343,74149,74150' represents: [64536,65343) and [74149,74150).
- RangeSet ranges = RangeSet::Parse(range_str);
- if (!ranges) {
- LOG(ERROR) << "Error parsing RangeSet string " << range_str;
- return false;
- }
-
+bool UpdateVerifier::ReadBlocks(const std::string partition_name,
+ const std::string& dm_block_device, const RangeSet& ranges) {
// RangeSet::Split() splits the ranges into multiple groups with same number of blocks (except for
// the last group).
size_t thread_num = std::thread::hardware_concurrency() ?: 4;
@@ -143,10 +128,10 @@
std::vector<std::future<bool>> threads;
for (const auto& group : groups) {
- auto thread_func = [&group, &dm_block_device, &partition]() {
+ auto thread_func = [&group, &dm_block_device, &partition_name]() {
android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(dm_block_device.c_str(), O_RDONLY)));
if (fd.get() == -1) {
- PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition;
+ PLOG(ERROR) << "Error reading " << dm_block_device << " for partition " << partition_name;
return false;
}
@@ -154,9 +139,7 @@
std::vector<uint8_t> buf(1024 * kBlockSize);
size_t block_count = 0;
- for (const auto& range : group) {
- size_t range_start = range.first;
- size_t range_end = range.second;
+ for (const auto& [range_start, range_end] : group) {
if (lseek64(fd.get(), static_cast<off64_t>(range_start) * kBlockSize, SEEK_SET) == -1) {
PLOG(ERROR) << "lseek to " << range_start << " failed";
return false;
@@ -189,47 +172,20 @@
return ret;
}
-// Returns true to indicate a passing verification (or the error should be ignored); Otherwise
-// returns false on fatal errors, where we should reject the current boot and trigger a fallback.
-// Note that update_verifier should be backward compatible to not reject care_map.txt from old
-// releases, which could otherwise fail to boot into the new release. For example, we've changed
-// the care_map format between N and O. An O update_verifier would fail to work with N
-// care_map.txt. This could be a result of sideloading an O OTA while the device having a pending N
-// update.
-bool verify_image(const std::string& care_map_name) {
- android::base::unique_fd care_map_fd(TEMP_FAILURE_RETRY(open(care_map_name.c_str(), O_RDONLY)));
- // If the device is flashed before the current boot, it may not have care_map.txt
- // in /data/ota_package. To allow the device to continue booting in this situation,
- // we should print a warning and skip the block verification.
- if (care_map_fd.get() == -1) {
- PLOG(WARNING) << "Failed to open " << care_map_name;
- return true;
- }
- // care_map file has up to six lines, where every two lines make a pair. Within each pair, the
- // first line has the partition name (e.g. "system"), while the second line holds the ranges of
- // all the blocks to verify.
- std::string file_content;
- if (!android::base::ReadFdToString(care_map_fd.get(), &file_content)) {
- LOG(ERROR) << "Error reading care map contents to string.";
+bool UpdateVerifier::VerifyPartitions() {
+ auto dm_block_devices = FindDmPartitions();
+ if (dm_block_devices.empty()) {
+ LOG(ERROR) << "No dm-enabled block device is found.";
return false;
}
- std::vector<std::string> lines;
- lines = android::base::Split(android::base::Trim(file_content), "\n");
- if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
- LOG(ERROR) << "Invalid lines in care_map: found " << lines.size()
- << " lines, expecting 2 or 4 or 6 lines.";
- return false;
- }
-
- for (size_t i = 0; i < lines.size(); i += 2) {
- // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
- // update_verifier (the last few metadata blocks can't be read via device mapper).
- if (android::base::StartsWith(lines[i], "/dev/block/")) {
- LOG(WARNING) << "Found legacy care_map.txt; skipped.";
- return true;
+ for (const auto& [partition_name, ranges] : partition_map_) {
+ if (dm_block_devices.find(partition_name) == dm_block_devices.end()) {
+ LOG(ERROR) << "Failed to find dm block device for " << partition_name;
+ return false;
}
- if (!read_blocks(lines[i], lines[i+1])) {
+
+ if (!ReadBlocks(partition_name, dm_block_devices.at(partition_name), ranges)) {
return false;
}
}
@@ -237,6 +193,133 @@
return true;
}
+bool UpdateVerifier::ParseCareMapPlainText(const std::string& content) {
+ // care_map file has up to six lines, where every two lines make a pair. Within each pair, the
+ // first line has the partition name (e.g. "system"), while the second line holds the ranges of
+ // all the blocks to verify.
+ auto lines = android::base::Split(android::base::Trim(content), "\n");
+ if (lines.size() != 2 && lines.size() != 4 && lines.size() != 6) {
+ LOG(WARNING) << "Invalid lines in care_map: found " << lines.size()
+ << " lines, expecting 2 or 4 or 6 lines.";
+ return false;
+ }
+
+ for (size_t i = 0; i < lines.size(); i += 2) {
+ const std::string& partition_name = lines[i];
+ const std::string& range_str = lines[i + 1];
+ // We're seeing an N care_map.txt. Skip the verification since it's not compatible with O
+ // update_verifier (the last few metadata blocks can't be read via device mapper).
+ if (android::base::StartsWith(partition_name, "/dev/block/")) {
+ LOG(WARNING) << "Found legacy care_map.txt; skipped.";
+ return false;
+ }
+
+ // For block range string, first integer 'count' equals 2 * total number of valid ranges,
+ // followed by 'count' number comma separated integers. Every two integers reprensent a
+ // block range with the first number included in range but second number not included.
+ // For example '4,64536,65343,74149,74150' represents: [64536,65343) and [74149,74150).
+ RangeSet ranges = RangeSet::Parse(range_str);
+ if (!ranges) {
+ LOG(WARNING) << "Error parsing RangeSet string " << range_str;
+ return false;
+ }
+
+ partition_map_.emplace(partition_name, ranges);
+ }
+
+ return true;
+}
+
+bool UpdateVerifier::ParseCareMap() {
+ partition_map_.clear();
+
+ std::string care_map_name = care_map_prefix_ + ".pb";
+ if (access(care_map_name.c_str(), R_OK) == -1) {
+ LOG(WARNING) << care_map_name
+ << " doesn't exist, falling back to read the care_map in plain text format.";
+ care_map_name = care_map_prefix_ + ".txt";
+ }
+
+ android::base::unique_fd care_map_fd(TEMP_FAILURE_RETRY(open(care_map_name.c_str(), O_RDONLY)));
+ // If the device is flashed before the current boot, it may not have care_map.txt in
+ // /data/ota_package. To allow the device to continue booting in this situation, we should
+ // print a warning and skip the block verification.
+ if (care_map_fd.get() == -1) {
+ PLOG(WARNING) << "Failed to open " << care_map_name;
+ return false;
+ }
+
+ std::string file_content;
+ if (!android::base::ReadFdToString(care_map_fd.get(), &file_content)) {
+ PLOG(WARNING) << "Failed to read " << care_map_name;
+ return false;
+ }
+
+ if (file_content.empty()) {
+ LOG(WARNING) << "Unexpected empty care map";
+ return false;
+ }
+
+ if (android::base::EndsWith(care_map_name, ".txt")) {
+ return ParseCareMapPlainText(file_content);
+ }
+
+ recovery_update_verifier::CareMap care_map;
+ if (!care_map.ParseFromString(file_content)) {
+ LOG(WARNING) << "Failed to parse " << care_map_name << " in protobuf format.";
+ return false;
+ }
+
+ for (const auto& partition : care_map.partitions()) {
+ if (partition.name().empty()) {
+ LOG(WARNING) << "Unexpected empty partition name.";
+ return false;
+ }
+ if (partition.ranges().empty()) {
+ LOG(WARNING) << "Unexpected block ranges for partition " << partition.name();
+ return false;
+ }
+ RangeSet ranges = RangeSet::Parse(partition.ranges());
+ if (!ranges) {
+ LOG(WARNING) << "Error parsing RangeSet string " << partition.ranges();
+ return false;
+ }
+
+ // Continues to check other partitions if there is a fingerprint mismatch.
+ if (partition.id().empty() || partition.id() == "unknown") {
+ LOG(WARNING) << "Skip reading partition " << partition.name()
+ << ": property_id is not provided to get fingerprint.";
+ continue;
+ }
+
+ std::string fingerprint = property_reader_(partition.id());
+ if (fingerprint != partition.fingerprint()) {
+ LOG(WARNING) << "Skip reading partition " << partition.name() << ": fingerprint "
+ << fingerprint << " doesn't match the expected value "
+ << partition.fingerprint();
+ continue;
+ }
+
+ partition_map_.emplace(partition.name(), ranges);
+ }
+
+ if (partition_map_.empty()) {
+ LOG(WARNING) << "No partition to verify";
+ return false;
+ }
+
+ return true;
+}
+
+void UpdateVerifier::set_care_map_prefix(const std::string& prefix) {
+ care_map_prefix_ = prefix;
+}
+
+void UpdateVerifier::set_property_reader(
+ const std::function<std::string(const std::string&)>& property_reader) {
+ property_reader_ = property_reader;
+}
+
static int reboot_device() {
if (android_reboot(ANDROID_RB_RESTART2, 0, nullptr) == -1) {
LOG(ERROR) << "Failed to reboot.";
@@ -264,19 +347,13 @@
if (is_successful == BoolResult::FALSE) {
// The current slot has not booted successfully.
-#if defined(PRODUCT_SUPPORTS_VERITY) || defined(BOARD_AVB_ENABLE)
bool skip_verification = false;
std::string verity_mode = android::base::GetProperty("ro.boot.veritymode", "");
if (verity_mode.empty()) {
- // With AVB it's possible to disable verification entirely and
- // in this case ro.boot.veritymode is empty.
-#if defined(BOARD_AVB_ENABLE)
- LOG(WARNING) << "verification has been disabled; marking without verification.";
+ // Skip the verification if ro.boot.veritymode property is not set. This could be a result
+ // that device doesn't support dm-verity, or has disabled that.
+ LOG(WARNING) << "dm-verity not enabled; marking without verification.";
skip_verification = true;
-#else
- LOG(ERROR) << "Failed to get dm-verity mode.";
- return reboot_device();
-#endif
} else if (android::base::EqualsIgnoreCase(verity_mode, "eio")) {
// We shouldn't see verity in EIO mode if the current slot hasn't booted successfully before.
// Continue the verification until we fail to read some blocks.
@@ -285,20 +362,19 @@
LOG(WARNING) << "dm-verity in disabled mode; marking without verification.";
skip_verification = true;
} else if (verity_mode != "enforcing") {
- LOG(ERROR) << "Unexpected dm-verity mode : " << verity_mode << ", expecting enforcing.";
+ LOG(ERROR) << "Unexpected dm-verity mode: " << verity_mode << ", expecting enforcing.";
return reboot_device();
}
if (!skip_verification) {
- static constexpr auto CARE_MAP_FILE = "/data/ota_package/care_map.txt";
- if (!verify_image(CARE_MAP_FILE)) {
+ UpdateVerifier verifier;
+ if (!verifier.ParseCareMap()) {
+ LOG(WARNING) << "Failed to parse the care map file, skipping verification";
+ } else if (!verifier.VerifyPartitions()) {
LOG(ERROR) << "Failed to verify all blocks in care map file.";
return reboot_device();
}
}
-#else
- LOG(WARNING) << "dm-verity not enabled; marking without verification.";
-#endif
CommandResult cr;
module->markBootSuccessful([&cr](CommandResult result) { cr = result; });
diff --git a/updater/Android.bp b/updater/Android.bp
new file mode 100644
index 0000000..c95ec5e
--- /dev/null
+++ b/updater/Android.bp
@@ -0,0 +1,79 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+cc_defaults {
+ name: "libupdater_defaults",
+
+ defaults: [
+ "recovery_defaults",
+ ],
+
+ static_libs: [
+ "libapplypatch",
+ "libbootloader_message",
+ "libbspatch",
+ "libedify",
+ "libotautil",
+ "libext4_utils",
+ "libfec",
+ "libfec_rs",
+ "libverity_tree",
+ "libfs_mgr",
+ "libgtest_prod",
+ "liblog",
+ "libselinux",
+ "libsparse",
+ "libsquashfs_utils",
+ "libbrotli",
+ "libbz",
+ "libziparchive",
+ "libz",
+ "libbase",
+ "libcrypto",
+ "libcrypto_utils",
+ "libcutils",
+ "libutils",
+ "libtune2fs",
+
+ "libext2_com_err",
+ "libext2_blkid",
+ "libext2_quota",
+ "libext2_uuid",
+ "libext2_e2p",
+ "libext2fs",
+ ],
+}
+
+cc_library_static {
+ name: "libupdater",
+
+ defaults: [
+ "recovery_defaults",
+ "libupdater_defaults",
+ ],
+
+ srcs: [
+ "blockimg.cpp",
+ "commands.cpp",
+ "install.cpp",
+ ],
+
+ include_dirs: [
+ "external/e2fsprogs/misc",
+ ],
+
+ export_include_dirs: [
+ "include",
+ ],
+}
diff --git a/updater/Android.mk b/updater/Android.mk
index 6f334ee..78e32ba 100644
--- a/updater/Android.mk
+++ b/updater/Android.mk
@@ -24,59 +24,32 @@
updater_common_static_libraries := \
libapplypatch \
+ libbootloader_message \
libbspatch \
libedify \
- libziparchive \
libotautil \
- libbootloader_message \
- libutils \
- libmounts \
- libotafault \
libext4_utils \
libfec \
libfec_rs \
+ libverity_tree \
libfs_mgr \
+ libgtest_prod \
liblog \
libselinux \
libsparse \
libsquashfs_utils \
+ libbrotli \
libbz \
+ libziparchive \
libz \
libbase \
libcrypto \
libcrypto_utils \
libcutils \
+ libutils \
libtune2fs \
- libbrotli \
$(tune2fs_static_libraries)
-# libupdater (static library)
-# ===============================
-include $(CLEAR_VARS)
-
-LOCAL_MODULE := libupdater
-
-LOCAL_SRC_FILES := \
- install.cpp \
- blockimg.cpp
-
-LOCAL_C_INCLUDES := \
- $(LOCAL_PATH)/.. \
- $(LOCAL_PATH)/include \
- external/e2fsprogs/misc
-
-LOCAL_CFLAGS := \
- -Wall \
- -Werror
-
-LOCAL_EXPORT_C_INCLUDE_DIRS := \
- $(LOCAL_PATH)/include
-
-LOCAL_STATIC_LIBRARIES := \
- $(updater_common_static_libraries)
-
-include $(BUILD_STATIC_LIBRARY)
-
# updater (static executable)
# ===============================
include $(CLEAR_VARS)
@@ -87,7 +60,6 @@
updater.cpp
LOCAL_C_INCLUDES := \
- $(LOCAL_PATH)/.. \
$(LOCAL_PATH)/include
LOCAL_CFLAGS := \
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index e93196b..8388456 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -15,8 +15,8 @@
*/
#include <ctype.h>
-#include <errno.h>
#include <dirent.h>
+#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/fs.h>
@@ -25,13 +25,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
-#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
-#include <fec/io.h>
#include <functional>
#include <limits>
@@ -47,16 +46,18 @@
#include <android-base/unique_fd.h>
#include <applypatch/applypatch.h>
#include <brotli/decode.h>
+#include <fec/io.h>
#include <openssl/sha.h>
#include <private/android_filesystem_config.h>
+#include <verity/hash_tree_builder.h>
#include <ziparchive/zip_archive.h>
#include "edify/expr.h"
-#include "otafault/ota_io.h"
-#include "otautil/cache_location.h"
#include "otautil/error_code.h"
+#include "otautil/paths.h"
#include "otautil/print_sha1.h"
#include "otautil/rangeset.h"
+#include "private/commands.h"
#include "updater/install.h"
#include "updater/updater.h"
@@ -74,7 +75,7 @@
static std::unordered_map<std::string, RangeSet> stash_map;
static void DeleteLastCommandFile() {
- std::string last_command_file = CacheLocation::location().last_command_file();
+ const std::string& last_command_file = Paths::Get().last_command_file();
if (unlink(last_command_file.c_str()) == -1 && errno != ENOENT) {
PLOG(ERROR) << "Failed to unlink: " << last_command_file;
}
@@ -82,8 +83,8 @@
// Parse the last command index of the last update and save the result to |last_command_index|.
// Return true if we successfully read the index.
-static bool ParseLastCommandFile(int* last_command_index) {
- std::string last_command_file = CacheLocation::location().last_command_file();
+static bool ParseLastCommandFile(size_t* last_command_index) {
+ const std::string& last_command_file = Paths::Get().last_command_file();
android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(last_command_file.c_str(), O_RDONLY)));
if (fd == -1) {
if (errno != ENOENT) {
@@ -116,10 +117,24 @@
return true;
}
-// Update the last command index in the last_command_file if the current command writes to the
-// stash either explicitly or implicitly.
-static bool UpdateLastCommandIndex(int command_index, const std::string& command_string) {
- std::string last_command_file = CacheLocation::location().last_command_file();
+static bool FsyncDir(const std::string& dirname) {
+ android::base::unique_fd dfd(TEMP_FAILURE_RETRY(open(dirname.c_str(), O_RDONLY | O_DIRECTORY)));
+ if (dfd == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
+ PLOG(ERROR) << "Failed to open " << dirname;
+ return false;
+ }
+ if (fsync(dfd) == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
+ PLOG(ERROR) << "Failed to fsync " << dirname;
+ return false;
+ }
+ return true;
+}
+
+// Update the last executed command index in the last_command_file.
+static bool UpdateLastCommandIndex(size_t command_index, const std::string& command_string) {
+ const std::string& last_command_file = Paths::Get().last_command_file();
std::string last_command_tmp = last_command_file + ".tmp";
std::string content = std::to_string(command_index) + "\n" + command_string;
android::base::unique_fd wfd(
@@ -144,61 +159,23 @@
return false;
}
- std::string last_command_dir = android::base::Dirname(last_command_file);
- android::base::unique_fd dfd(
- TEMP_FAILURE_RETRY(ota_open(last_command_dir.c_str(), O_RDONLY | O_DIRECTORY)));
- if (dfd == -1) {
- PLOG(ERROR) << "Failed to open " << last_command_dir;
- return false;
- }
-
- if (fsync(dfd) == -1) {
- PLOG(ERROR) << "Failed to fsync " << last_command_dir;
+ if (!FsyncDir(android::base::Dirname(last_command_file))) {
return false;
}
return true;
}
-static int read_all(int fd, uint8_t* data, size_t size) {
- size_t so_far = 0;
- while (so_far < size) {
- ssize_t r = TEMP_FAILURE_RETRY(ota_read(fd, data+so_far, size-so_far));
- if (r == -1) {
- failure_type = kFreadFailure;
- PLOG(ERROR) << "read failed";
- return -1;
- } else if (r == 0) {
- failure_type = kFreadFailure;
- LOG(ERROR) << "read reached unexpected EOF.";
- return -1;
- }
- so_far += r;
- }
- return 0;
-}
-
-static int read_all(int fd, std::vector<uint8_t>& buffer, size_t size) {
- return read_all(fd, buffer.data(), size);
-}
-
-static int write_all(int fd, const uint8_t* data, size_t size) {
- size_t written = 0;
- while (written < size) {
- ssize_t w = TEMP_FAILURE_RETRY(ota_write(fd, data+written, size-written));
- if (w == -1) {
- failure_type = kFwriteFailure;
- PLOG(ERROR) << "write failed";
- return -1;
- }
- written += w;
- }
-
- return 0;
-}
-
-static int write_all(int fd, const std::vector<uint8_t>& buffer, size_t size) {
- return write_all(fd, buffer.data(), size);
+static bool SetPartitionUpdatedMarker(const std::string& marker) {
+ if (!android::base::WriteStringToFile("", marker)) {
+ PLOG(ERROR) << "Failed to write to marker file " << marker;
+ return false;
+ }
+ if (!FsyncDir(android::base::Dirname(marker))) {
+ return false;
+ }
+ LOG(INFO) << "Wrote partition updated marker to " << marker;
+ return true;
}
static bool discard_blocks(int fd, off64_t offset, uint64_t size) {
@@ -225,11 +202,10 @@
return true;
}
-static void allocate(size_t size, std::vector<uint8_t>& buffer) {
- // if the buffer's big enough, reuse it.
- if (size <= buffer.size()) return;
-
- buffer.resize(size);
+static void allocate(size_t size, std::vector<uint8_t>* buffer) {
+ // If the buffer's big enough, reuse it.
+ if (size <= buffer->size()) return;
+ buffer->resize(size);
}
/**
@@ -274,7 +250,9 @@
write_now = current_range_left_;
}
- if (write_all(fd_, data, write_now) == -1) {
+ if (!android::base::WriteFully(fd_, data, write_now)) {
+ failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+ PLOG(ERROR) << "Failed to write " << write_now << " bytes of data";
break;
}
@@ -483,15 +461,17 @@
return nullptr;
}
-static int ReadBlocks(const RangeSet& src, std::vector<uint8_t>& buffer, int fd) {
+static int ReadBlocks(const RangeSet& src, std::vector<uint8_t>* buffer, int fd) {
size_t p = 0;
- for (const auto& range : src) {
- if (!check_lseek(fd, static_cast<off64_t>(range.first) * BLOCKSIZE, SEEK_SET)) {
+ for (const auto& [begin, end] : src) {
+ if (!check_lseek(fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
return -1;
}
- size_t size = (range.second - range.first) * BLOCKSIZE;
- if (read_all(fd, buffer.data() + p, size) == -1) {
+ size_t size = (end - begin) * BLOCKSIZE;
+ if (!android::base::ReadFully(fd, buffer->data() + p, size)) {
+ failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+ PLOG(ERROR) << "Failed to read " << size << " bytes of data";
return -1;
}
@@ -503,9 +483,9 @@
static int WriteBlocks(const RangeSet& tgt, const std::vector<uint8_t>& buffer, int fd) {
size_t written = 0;
- for (const auto& range : tgt) {
- off64_t offset = static_cast<off64_t>(range.first) * BLOCKSIZE;
- size_t size = (range.second - range.first) * BLOCKSIZE;
+ for (const auto& [begin, end] : tgt) {
+ off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE;
+ size_t size = (end - begin) * BLOCKSIZE;
if (!discard_blocks(fd, offset, size)) {
return -1;
}
@@ -514,7 +494,9 @@
return -1;
}
- if (write_all(fd, buffer.data() + written, size) == -1) {
+ if (!android::base::WriteFully(fd, buffer.data() + written, size)) {
+ failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+ PLOG(ERROR) << "Failed to write " << size << " bytes of data";
return -1;
}
@@ -528,9 +510,8 @@
struct CommandParameters {
std::vector<std::string> tokens;
size_t cpos;
- int cmdindex;
- const char* cmdname;
- const char* cmdline;
+ std::string cmdname;
+ std::string cmdline;
std::string freestash;
std::string stashbase;
bool canwrite;
@@ -644,43 +625,43 @@
LOG(INFO) << "print hash in hex for source blocks in missing stash: " << id;
const RangeSet& src = stash_map[id];
std::vector<uint8_t> buffer(src.blocks() * BLOCKSIZE);
- if (ReadBlocks(src, buffer, fd) == -1) {
- LOG(ERROR) << "failed to read source blocks for stash: " << id;
- return;
+ if (ReadBlocks(src, &buffer, fd) == -1) {
+ LOG(ERROR) << "failed to read source blocks for stash: " << id;
+ return;
}
PrintHashForCorruptedStashedBlocks(id, buffer, src);
}
static int VerifyBlocks(const std::string& expected, const std::vector<uint8_t>& buffer,
- const size_t blocks, bool printerror) {
- uint8_t digest[SHA_DIGEST_LENGTH];
- const uint8_t* data = buffer.data();
+ const size_t blocks, bool printerror) {
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ const uint8_t* data = buffer.data();
- SHA1(data, blocks * BLOCKSIZE, digest);
+ SHA1(data, blocks * BLOCKSIZE, digest);
- std::string hexdigest = print_sha1(digest);
+ std::string hexdigest = print_sha1(digest);
- if (hexdigest != expected) {
- if (printerror) {
- LOG(ERROR) << "failed to verify blocks (expected " << expected << ", read "
- << hexdigest << ")";
- }
- return -1;
+ if (hexdigest != expected) {
+ if (printerror) {
+ LOG(ERROR) << "failed to verify blocks (expected " << expected << ", read " << hexdigest
+ << ")";
}
+ return -1;
+ }
- return 0;
+ return 0;
}
static std::string GetStashFileName(const std::string& base, const std::string& id,
- const std::string& postfix) {
- if (base.empty()) {
- return "";
- }
-
- std::string fn(CacheLocation::location().stash_directory_base());
- fn += "/" + base + "/" + id + postfix;
-
- return fn;
+ const std::string& postfix) {
+ if (base.empty()) {
+ return "";
+ }
+ std::string filename = Paths::Get().stash_directory_base() + "/" + base;
+ if (id.empty() && postfix.empty()) {
+ return filename;
+ }
+ return filename + "/" + id + postfix;
}
// Does a best effort enumeration of stash files. Ignores possible non-file items in the stash
@@ -733,8 +714,8 @@
}
}
-static int LoadStash(CommandParameters& params, const std::string& id, bool verify, size_t* blocks,
- std::vector<uint8_t>& buffer, bool printnoent) {
+static int LoadStash(const CommandParameters& params, const std::string& id, bool verify,
+ std::vector<uint8_t>* buffer, bool printnoent) {
// In verify mode, if source range_set was saved for the given hash, check contents in the source
// blocks first. If the check fails, search for the stashed files on /cache as usual.
if (!params.canwrite) {
@@ -746,20 +727,17 @@
LOG(ERROR) << "failed to read source blocks in stash map.";
return -1;
}
- if (VerifyBlocks(id, buffer, src.blocks(), true) != 0) {
+ if (VerifyBlocks(id, *buffer, src.blocks(), true) != 0) {
LOG(ERROR) << "failed to verify loaded source blocks in stash map.";
- PrintHashForCorruptedStashedBlocks(id, buffer, src);
+ if (!is_retry) {
+ PrintHashForCorruptedStashedBlocks(id, *buffer, src);
+ }
return -1;
}
return 0;
}
}
- size_t blockcount = 0;
- if (!blocks) {
- blocks = &blockcount;
- }
-
std::string fn = GetStashFileName(params.stashbase, id, "");
struct stat sb;
@@ -778,28 +756,30 @@
return -1;
}
- android::base::unique_fd fd(TEMP_FAILURE_RETRY(ota_open(fn.c_str(), O_RDONLY)));
+ android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(fn.c_str(), O_RDONLY)));
if (fd == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
PLOG(ERROR) << "open \"" << fn << "\" failed";
return -1;
}
allocate(sb.st_size, buffer);
- if (read_all(fd, buffer, sb.st_size) == -1) {
+ if (!android::base::ReadFully(fd, buffer->data(), sb.st_size)) {
+ failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+ PLOG(ERROR) << "Failed to read " << sb.st_size << " bytes of data";
return -1;
}
- *blocks = sb.st_size / BLOCKSIZE;
-
- if (verify && VerifyBlocks(id, buffer, *blocks, true) != 0) {
+ size_t blocks = sb.st_size / BLOCKSIZE;
+ if (verify && VerifyBlocks(id, *buffer, blocks, true) != 0) {
LOG(ERROR) << "unexpected contents in " << fn;
if (stash_map.find(id) == stash_map.end()) {
LOG(ERROR) << "failed to find source blocks number for stash " << id
<< " when executing command: " << params.cmdname;
} else {
const RangeSet& src = stash_map[id];
- PrintHashForCorruptedStashedBlocks(id, buffer, src);
+ PrintHashForCorruptedStashedBlocks(id, *buffer, src);
}
DeleteFile(fn);
return -1;
@@ -809,109 +789,90 @@
}
static int WriteStash(const std::string& base, const std::string& id, int blocks,
- std::vector<uint8_t>& buffer, bool checkspace, bool* exists) {
- if (base.empty()) {
- return -1;
+ const std::vector<uint8_t>& buffer, bool checkspace, bool* exists) {
+ if (base.empty()) {
+ return -1;
+ }
+
+ if (checkspace && !CheckAndFreeSpaceOnCache(blocks * BLOCKSIZE)) {
+ LOG(ERROR) << "not enough space to write stash";
+ return -1;
+ }
+
+ std::string fn = GetStashFileName(base, id, ".partial");
+ std::string cn = GetStashFileName(base, id, "");
+
+ if (exists) {
+ struct stat sb;
+ int res = stat(cn.c_str(), &sb);
+
+ if (res == 0) {
+ // The file already exists and since the name is the hash of the contents,
+ // it's safe to assume the contents are identical (accidental hash collisions
+ // are unlikely)
+ LOG(INFO) << " skipping " << blocks << " existing blocks in " << cn;
+ *exists = true;
+ return 0;
}
- if (checkspace && CacheSizeCheck(blocks * BLOCKSIZE) != 0) {
- LOG(ERROR) << "not enough space to write stash";
- return -1;
- }
+ *exists = false;
+ }
- std::string fn = GetStashFileName(base, id, ".partial");
- std::string cn = GetStashFileName(base, id, "");
+ LOG(INFO) << " writing " << blocks << " blocks to " << cn;
- if (exists) {
- struct stat sb;
- int res = stat(cn.c_str(), &sb);
+ android::base::unique_fd fd(
+ TEMP_FAILURE_RETRY(open(fn.c_str(), O_WRONLY | O_CREAT | O_TRUNC, STASH_FILE_MODE)));
+ if (fd == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
+ PLOG(ERROR) << "failed to create \"" << fn << "\"";
+ return -1;
+ }
- if (res == 0) {
- // The file already exists and since the name is the hash of the contents,
- // it's safe to assume the contents are identical (accidental hash collisions
- // are unlikely)
- LOG(INFO) << " skipping " << blocks << " existing blocks in " << cn;
- *exists = true;
- return 0;
- }
+ if (fchown(fd, AID_SYSTEM, AID_SYSTEM) != 0) { // system user
+ PLOG(ERROR) << "failed to chown \"" << fn << "\"";
+ return -1;
+ }
- *exists = false;
- }
+ if (!android::base::WriteFully(fd, buffer.data(), blocks * BLOCKSIZE)) {
+ failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+ PLOG(ERROR) << "Failed to write " << blocks * BLOCKSIZE << " bytes of data";
+ return -1;
+ }
- LOG(INFO) << " writing " << blocks << " blocks to " << cn;
+ if (fsync(fd) == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
+ PLOG(ERROR) << "fsync \"" << fn << "\" failed";
+ return -1;
+ }
- android::base::unique_fd fd(
- TEMP_FAILURE_RETRY(ota_open(fn.c_str(), O_WRONLY | O_CREAT | O_TRUNC, STASH_FILE_MODE)));
- if (fd == -1) {
- PLOG(ERROR) << "failed to create \"" << fn << "\"";
- return -1;
- }
+ if (rename(fn.c_str(), cn.c_str()) == -1) {
+ PLOG(ERROR) << "rename(\"" << fn << "\", \"" << cn << "\") failed";
+ return -1;
+ }
- if (fchown(fd, AID_SYSTEM, AID_SYSTEM) != 0) { // system user
- PLOG(ERROR) << "failed to chown \"" << fn << "\"";
- return -1;
- }
+ std::string dname = GetStashFileName(base, "", "");
+ if (!FsyncDir(dname)) {
+ return -1;
+ }
- if (write_all(fd, buffer, blocks * BLOCKSIZE) == -1) {
- return -1;
- }
-
- if (ota_fsync(fd) == -1) {
- failure_type = kFsyncFailure;
- PLOG(ERROR) << "fsync \"" << fn << "\" failed";
- return -1;
- }
-
- if (rename(fn.c_str(), cn.c_str()) == -1) {
- PLOG(ERROR) << "rename(\"" << fn << "\", \"" << cn << "\") failed";
- return -1;
- }
-
- std::string dname = GetStashFileName(base, "", "");
- android::base::unique_fd dfd(TEMP_FAILURE_RETRY(ota_open(dname.c_str(),
- O_RDONLY | O_DIRECTORY)));
- if (dfd == -1) {
- failure_type = kFileOpenFailure;
- PLOG(ERROR) << "failed to open \"" << dname << "\" failed";
- return -1;
- }
-
- if (ota_fsync(dfd) == -1) {
- failure_type = kFsyncFailure;
- PLOG(ERROR) << "fsync \"" << dname << "\" failed";
- return -1;
- }
-
- return 0;
+ return 0;
}
// Creates a directory for storing stash files and checks if the /cache partition
// hash enough space for the expected amount of blocks we need to store. Returns
// >0 if we created the directory, zero if it existed already, and <0 of failure.
-
-static int CreateStash(State* state, size_t maxblocks, const std::string& blockdev,
- std::string& base) {
- if (blockdev.empty()) {
- return -1;
- }
-
- // Stash directory should be different for each partition to avoid conflicts
- // when updating multiple partitions at the same time, so we use the hash of
- // the block device name as the base directory
- uint8_t digest[SHA_DIGEST_LENGTH];
- SHA1(reinterpret_cast<const uint8_t*>(blockdev.data()), blockdev.size(), digest);
- base = print_sha1(digest);
-
+static int CreateStash(State* state, size_t maxblocks, const std::string& base) {
std::string dirname = GetStashFileName(base, "", "");
struct stat sb;
int res = stat(dirname.c_str(), &sb);
- size_t max_stash_size = maxblocks * BLOCKSIZE;
-
if (res == -1 && errno != ENOENT) {
ErrorAbort(state, kStashCreationFailure, "stat \"%s\" failed: %s", dirname.c_str(),
strerror(errno));
return -1;
- } else if (res != 0) {
+ }
+
+ size_t max_stash_size = maxblocks * BLOCKSIZE;
+ if (res == -1) {
LOG(INFO) << "creating stash " << dirname;
res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE);
@@ -927,7 +888,7 @@
return -1;
}
- if (CacheSizeCheck(max_stash_size) != 0) {
+ if (!CheckAndFreeSpaceOnCache(max_stash_size)) {
ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu needed)",
max_stash_size);
return -1;
@@ -959,7 +920,7 @@
if (max_stash_size > existing) {
size_t needed = max_stash_size - existing;
- if (CacheSizeCheck(needed) != 0) {
+ if (!CheckAndFreeSpaceOnCache(needed)) {
ErrorAbort(state, kStashCreationFailure, "not enough space for stash (%zu more needed)",
needed);
return -1;
@@ -1023,7 +984,7 @@
return -1;
}
- allocate(*src_blocks * BLOCKSIZE, params.buffer);
+ allocate(*src_blocks * BLOCKSIZE, ¶ms.buffer);
// "-" or <src_range> [<src_loc>]
if (params.tokens[params.cpos] == "-") {
@@ -1034,7 +995,7 @@
CHECK(static_cast<bool>(src));
*overlap = src.Overlaps(tgt);
- if (ReadBlocks(src, params.buffer, params.fd) == -1) {
+ if (ReadBlocks(src, ¶ms.buffer, params.fd) == -1) {
return -1;
}
@@ -1059,7 +1020,7 @@
}
std::vector<uint8_t> stash;
- if (LoadStash(params, tokens[0], false, nullptr, stash, true) == -1) {
+ if (LoadStash(params, tokens[0], false, &stash, true) == -1) {
// These source blocks will fail verification if used later, but we
// will let the caller decide if this is a fatal failure
LOG(ERROR) << "failed to load stash " << tokens[0];
@@ -1100,10 +1061,9 @@
*
* If the return value is 0, source blocks have expected content and the command can be performed.
*/
-static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet& tgt, size_t* src_blocks,
- bool onehash, bool* overlap) {
+static int LoadSrcTgtVersion3(CommandParameters& params, RangeSet* tgt, size_t* src_blocks,
+ bool onehash) {
CHECK(src_blocks != nullptr);
- CHECK(overlap != nullptr);
if (params.cpos >= params.tokens.size()) {
LOG(ERROR) << "missing source hash";
@@ -1131,29 +1091,30 @@
}
// <tgt_range>
- tgt = RangeSet::Parse(params.tokens[params.cpos++]);
- CHECK(static_cast<bool>(tgt));
+ *tgt = RangeSet::Parse(params.tokens[params.cpos++]);
+ CHECK(static_cast<bool>(*tgt));
- std::vector<uint8_t> tgtbuffer(tgt.blocks() * BLOCKSIZE);
- if (ReadBlocks(tgt, tgtbuffer, params.fd) == -1) {
+ std::vector<uint8_t> tgtbuffer(tgt->blocks() * BLOCKSIZE);
+ if (ReadBlocks(*tgt, &tgtbuffer, params.fd) == -1) {
return -1;
}
// Return now if target blocks already have expected content.
- if (VerifyBlocks(tgthash, tgtbuffer, tgt.blocks(), false) == 0) {
+ if (VerifyBlocks(tgthash, tgtbuffer, tgt->blocks(), false) == 0) {
return 1;
}
// Load source blocks.
- if (LoadSourceBlocks(params, tgt, src_blocks, overlap) == -1) {
+ bool overlap = false;
+ if (LoadSourceBlocks(params, *tgt, src_blocks, &overlap) == -1) {
return -1;
}
if (VerifyBlocks(srchash, params.buffer, *src_blocks, true) == 0) {
- // If source and target blocks overlap, stash the source blocks so we can
- // resume from possible write errors. In verify mode, we can skip stashing
- // because the source blocks won't be overwritten.
- if (*overlap && params.canwrite) {
+ // If source and target blocks overlap, stash the source blocks so we can resume from possible
+ // write errors. In verify mode, we can skip stashing because the source blocks won't be
+ // overwritten.
+ if (overlap && params.canwrite) {
LOG(INFO) << "stashing " << *src_blocks << " overlapping blocks to " << srchash;
bool stash_exists = false;
@@ -1163,10 +1124,6 @@
return -1;
}
- if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) {
- LOG(WARNING) << "Failed to update the last command file.";
- }
-
params.stashed += *src_blocks;
// Can be deleted when the write has completed.
if (!stash_exists) {
@@ -1178,7 +1135,7 @@
return 0;
}
- if (*overlap && LoadStash(params, srchash, true, nullptr, params.buffer, true) == 0) {
+ if (overlap && LoadStash(params, srchash, true, ¶ms.buffer, true) == 0) {
// Overlapping source blocks were previously stashed, command can proceed. We are recovering
// from an interrupted command, so we don't know if the stash can safely be deleted after this
// command.
@@ -1196,9 +1153,8 @@
static int PerformCommandMove(CommandParameters& params) {
size_t blocks = 0;
- bool overlap = false;
RangeSet tgt;
- int status = LoadSrcTgtVersion3(params, tgt, &blocks, true, &overlap);
+ int status = LoadSrcTgtVersion3(params, &tgt, &blocks, true);
if (status == -1) {
LOG(ERROR) << "failed to read blocks for move";
@@ -1244,8 +1200,7 @@
}
const std::string& id = params.tokens[params.cpos++];
- size_t blocks = 0;
- if (LoadStash(params, id, true, &blocks, params.buffer, false) == 0) {
+ if (LoadStash(params, id, true, ¶ms.buffer, false) == 0) {
// Stash file already exists and has expected contents. Do not read from source again, as the
// source may have been already overwritten during a previous attempt.
return 0;
@@ -1254,11 +1209,11 @@
RangeSet src = RangeSet::Parse(params.tokens[params.cpos++]);
CHECK(static_cast<bool>(src));
- allocate(src.blocks() * BLOCKSIZE, params.buffer);
- if (ReadBlocks(src, params.buffer, params.fd) == -1) {
+ size_t blocks = src.blocks();
+ allocate(blocks * BLOCKSIZE, ¶ms.buffer);
+ if (ReadBlocks(src, ¶ms.buffer, params.fd) == -1) {
return -1;
}
- blocks = src.blocks();
stash_map[id] = src;
if (VerifyBlocks(id, params.buffer, blocks, true) != 0) {
@@ -1277,10 +1232,6 @@
LOG(INFO) << "stashing " << blocks << " blocks to " << id;
int result = WriteStash(params.stashbase, id, blocks, params.buffer, false, nullptr);
if (result == 0) {
- if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) {
- LOG(WARNING) << "Failed to update the last command file.";
- }
-
params.stashed += blocks;
}
return result;
@@ -1314,13 +1265,13 @@
LOG(INFO) << " zeroing " << tgt.blocks() << " blocks";
- allocate(BLOCKSIZE, params.buffer);
+ allocate(BLOCKSIZE, ¶ms.buffer);
memset(params.buffer.data(), 0, BLOCKSIZE);
if (params.canwrite) {
- for (const auto& range : tgt) {
- off64_t offset = static_cast<off64_t>(range.first) * BLOCKSIZE;
- size_t size = (range.second - range.first) * BLOCKSIZE;
+ for (const auto& [begin, end] : tgt) {
+ off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE;
+ size_t size = (end - begin) * BLOCKSIZE;
if (!discard_blocks(params.fd, offset, size)) {
return -1;
}
@@ -1329,8 +1280,10 @@
return -1;
}
- for (size_t j = range.first; j < range.second; ++j) {
- if (write_all(params.fd, params.buffer, BLOCKSIZE) == -1) {
+ for (size_t j = begin; j < end; ++j) {
+ if (!android::base::WriteFully(params.fd, params.buffer.data(), BLOCKSIZE)) {
+ failure_type = errno == EIO ? kEioFailure : kFwriteFailure;
+ PLOG(ERROR) << "Failed to write " << BLOCKSIZE << " bytes of data";
return -1;
}
}
@@ -1401,8 +1354,7 @@
RangeSet tgt;
size_t blocks = 0;
- bool overlap = false;
- int status = LoadSrcTgtVersion3(params, tgt, &blocks, false, &overlap);
+ int status = LoadSrcTgtVersion3(params, &tgt, &blocks, false);
if (status == -1) {
LOG(ERROR) << "failed to read blocks for diff";
@@ -1422,14 +1374,15 @@
if (status == 0) {
LOG(INFO) << "patching " << blocks << " blocks to " << tgt.blocks();
Value patch_value(
- VAL_BLOB, std::string(reinterpret_cast<const char*>(params.patch_start + offset), len));
+ Value::Type::BLOB,
+ std::string(reinterpret_cast<const char*>(params.patch_start + offset), len));
RangeSinkWriter writer(params.fd, tgt);
if (params.cmdname[0] == 'i') { // imgdiff
if (ApplyImagePatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value,
std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
std::placeholders::_2),
- nullptr, nullptr) != 0) {
+ nullptr) != 0) {
LOG(ERROR) << "Failed to apply image patch.";
failure_type = kPatchApplicationFailure;
return -1;
@@ -1437,8 +1390,7 @@
} else {
if (ApplyBSDiffPatch(params.buffer.data(), blocks * BLOCKSIZE, patch_value, 0,
std::bind(&RangeSinkWriter::Write, &writer, std::placeholders::_1,
- std::placeholders::_2),
- nullptr) != 0) {
+ std::placeholders::_2)) != 0) {
LOG(ERROR) << "Failed to apply bsdiff patch.";
failure_type = kPatchApplicationFailure;
return -1;
@@ -1492,12 +1444,12 @@
if (params.canwrite) {
LOG(INFO) << " erasing " << tgt.blocks() << " blocks";
- for (const auto& range : tgt) {
+ for (const auto& [begin, end] : tgt) {
uint64_t blocks[2];
// offset in bytes
- blocks[0] = range.first * static_cast<uint64_t>(BLOCKSIZE);
+ blocks[0] = begin * static_cast<uint64_t>(BLOCKSIZE);
// length in bytes
- blocks[1] = (range.second - range.first) * static_cast<uint64_t>(BLOCKSIZE);
+ blocks[1] = (end - begin) * static_cast<uint64_t>(BLOCKSIZE);
if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) {
PLOG(ERROR) << "BLKDISCARD ioctl failed";
@@ -1509,24 +1461,120 @@
return 0;
}
-// Definitions for transfer list command functions
-typedef int (*CommandFunction)(CommandParameters&);
+static int PerformCommandAbort(CommandParameters&) {
+ LOG(INFO) << "Aborting as instructed";
+ return -1;
+}
-struct Command {
- const char* name;
- CommandFunction f;
-};
+// Computes the hash_tree bytes based on the parameters, checks if the root hash of the tree
+// matches the expected hash and writes the result to the specified range on the block_device.
+// Hash_tree computation arguments:
+// hash_tree_ranges
+// source_ranges
+// hash_algorithm
+// salt_hex
+// root_hash
+static int PerformCommandComputeHashTree(CommandParameters& params) {
+ if (params.cpos + 5 != params.tokens.size()) {
+ LOG(ERROR) << "Invaild arguments count in hash computation " << params.cmdline;
+ return -1;
+ }
-// args:
-// - block device (or file) to modify in-place
-// - transfer list (blob)
-// - new data stream (filename within package.zip)
-// - patch stream (filename within package.zip, must be uncompressed)
+ // Expects the hash_tree data to be contiguous.
+ RangeSet hash_tree_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
+ if (!hash_tree_ranges || hash_tree_ranges.size() != 1) {
+ LOG(ERROR) << "Invalid hash tree ranges in " << params.cmdline;
+ return -1;
+ }
+
+ RangeSet source_ranges = RangeSet::Parse(params.tokens[params.cpos++]);
+ if (!source_ranges) {
+ LOG(ERROR) << "Invalid source ranges in " << params.cmdline;
+ return -1;
+ }
+
+ auto hash_function = HashTreeBuilder::HashFunction(params.tokens[params.cpos++]);
+ if (hash_function == nullptr) {
+ LOG(ERROR) << "Invalid hash algorithm in " << params.cmdline;
+ return -1;
+ }
+
+ std::vector<unsigned char> salt;
+ std::string salt_hex = params.tokens[params.cpos++];
+ if (salt_hex.empty() || !HashTreeBuilder::ParseBytesArrayFromString(salt_hex, &salt)) {
+ LOG(ERROR) << "Failed to parse salt in " << params.cmdline;
+ return -1;
+ }
+
+ std::string expected_root_hash = params.tokens[params.cpos++];
+ if (expected_root_hash.empty()) {
+ LOG(ERROR) << "Invalid root hash in " << params.cmdline;
+ return -1;
+ }
+
+ // Starts the hash_tree computation.
+ HashTreeBuilder builder(BLOCKSIZE, hash_function);
+ if (!builder.Initialize(source_ranges.blocks() * BLOCKSIZE, salt)) {
+ LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString()
+ << ", salt " << salt_hex;
+ return -1;
+ }
+
+ // Iterates through every block in the source_ranges and updates the hash tree structure
+ // accordingly.
+ for (const auto& [begin, end] : source_ranges) {
+ uint8_t buffer[BLOCKSIZE];
+ if (!check_lseek(params.fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
+ PLOG(ERROR) << "Failed to seek to block: " << begin;
+ return -1;
+ }
+
+ for (size_t i = begin; i < end; i++) {
+ if (!android::base::ReadFully(params.fd, buffer, BLOCKSIZE)) {
+ failure_type = errno == EIO ? kEioFailure : kFreadFailure;
+ LOG(ERROR) << "Failed to read data in " << begin << ":" << end;
+ return -1;
+ }
+
+ if (!builder.Update(reinterpret_cast<unsigned char*>(buffer), BLOCKSIZE)) {
+ LOG(ERROR) << "Failed to update hash tree builder";
+ return -1;
+ }
+ }
+ }
+
+ if (!builder.BuildHashTree()) {
+ LOG(ERROR) << "Failed to build hash tree";
+ return -1;
+ }
+
+ std::string root_hash_hex = HashTreeBuilder::BytesArrayToString(builder.root_hash());
+ if (root_hash_hex != expected_root_hash) {
+ LOG(ERROR) << "Root hash of the verity hash tree doesn't match the expected value. Expected: "
+ << expected_root_hash << ", actual: " << root_hash_hex;
+ return -1;
+ }
+
+ uint64_t write_offset = static_cast<uint64_t>(hash_tree_ranges.GetBlockNumber(0)) * BLOCKSIZE;
+ if (params.canwrite && !builder.WriteHashTreeToFd(params.fd, write_offset)) {
+ LOG(ERROR) << "Failed to write hash tree to output";
+ return -1;
+ }
+
+ // TODO(xunchang) validates the written bytes
+
+ return 0;
+}
+
+using CommandFunction = std::function<int(CommandParameters&)>;
+
+using CommandMap = std::unordered_map<Command::Type, CommandFunction>;
static Value* PerformBlockImageUpdate(const char* name, State* state,
const std::vector<std::unique_ptr<Expr>>& argv,
- const Command* commands, size_t cmdcount, bool dryrun) {
+ const CommandMap& command_map, bool dryrun) {
CommandParameters params = {};
+ stash_map.clear();
params.canwrite = !dryrun;
LOG(INFO) << "performing " << (dryrun ? "verification" : "update");
@@ -1545,24 +1593,29 @@
return nullptr;
}
+ // args:
+ // - block device (or file) to modify in-place
+ // - transfer list (blob)
+ // - new data stream (filename within package.zip)
+ // - patch stream (filename within package.zip, must be uncompressed)
const std::unique_ptr<Value>& blockdev_filename = args[0];
const std::unique_ptr<Value>& transfer_list_value = args[1];
const std::unique_ptr<Value>& new_data_fn = args[2];
const std::unique_ptr<Value>& patch_data_fn = args[3];
- if (blockdev_filename->type != VAL_STRING) {
+ if (blockdev_filename->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name);
return StringValue("");
}
- if (transfer_list_value->type != VAL_BLOB) {
+ if (transfer_list_value->type != Value::Type::BLOB) {
ErrorAbort(state, kArgsParsingFailure, "transfer_list argument to %s must be blob", name);
return StringValue("");
}
- if (new_data_fn->type != VAL_STRING) {
+ if (new_data_fn->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "new_data_fn argument to %s must be string", name);
return StringValue("");
}
- if (patch_data_fn->type != VAL_STRING) {
+ if (patch_data_fn->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "patch_data_fn argument to %s must be string", name);
return StringValue("");
}
@@ -1594,38 +1647,46 @@
return StringValue("");
}
- params.fd.reset(TEMP_FAILURE_RETRY(ota_open(blockdev_filename->data.c_str(), O_RDWR)));
+ params.fd.reset(TEMP_FAILURE_RETRY(open(blockdev_filename->data.c_str(), O_RDWR)));
if (params.fd == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFileOpenFailure;
PLOG(ERROR) << "open \"" << blockdev_filename->data << "\" failed";
return StringValue("");
}
- if (params.canwrite) {
- params.nti.za = za;
- params.nti.entry = new_entry;
- params.nti.brotli_compressed = android::base::EndsWith(new_data_fn->data, ".br");
- if (params.nti.brotli_compressed) {
- // Initialize brotli decoder state.
- params.nti.brotli_decoder_state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
+ // 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);
+ params.stashbase = print_sha1(digest);
+
+ // Possibly do return early on retry, by checking the marker. If the update on this partition has
+ // been finished (but interrupted at a later point), there could be leftover on /cache that would
+ // fail the no-op retry.
+ std::string updated_marker = GetStashFileName(params.stashbase + ".UPDATED", "", "");
+ if (is_retry) {
+ struct stat sb;
+ int result = stat(updated_marker.c_str(), &sb);
+ if (result == 0) {
+ LOG(INFO) << "Skipping already updated partition " << blockdev_filename->data
+ << " based on marker";
+ return StringValue("t");
}
- params.nti.receiver_available = true;
-
- pthread_mutex_init(¶ms.nti.mu, nullptr);
- pthread_cond_init(¶ms.nti.cv, nullptr);
- pthread_attr_t attr;
- pthread_attr_init(&attr);
- pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
-
- int error = pthread_create(¶ms.thread, &attr, unzip_new_data, ¶ms.nti);
- if (error != 0) {
- PLOG(ERROR) << "pthread_create failed";
+ } else {
+ // Delete the obsolete marker if any.
+ std::string err;
+ if (!android::base::RemoveFileIfExists(updated_marker, &err)) {
+ LOG(ERROR) << "Failed to remove partition updated marker " << updated_marker << ": " << err;
return StringValue("");
}
}
+ static constexpr size_t kTransferListHeaderLines = 4;
std::vector<std::string> lines = android::base::Split(transfer_list_value->data, "\n");
- if (lines.size() < 2) {
- ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zd]",
+ if (lines.size() < kTransferListHeaderLines) {
+ ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]",
lines.size());
return StringValue("");
}
@@ -1649,13 +1710,6 @@
return StringValue("t");
}
- size_t start = 2;
- if (lines.size() < 4) {
- ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]",
- lines.size());
- return StringValue("");
- }
-
// Third line is how many stash entries are needed simultaneously.
LOG(INFO) << "maximum stash entries " << lines[2];
@@ -1667,15 +1721,38 @@
return StringValue("");
}
- int res = CreateStash(state, stash_max_blocks, blockdev_filename->data, params.stashbase);
+ int res = CreateStash(state, stash_max_blocks, params.stashbase);
if (res == -1) {
return StringValue("");
}
-
params.createdstash = res;
- // When performing an update, save the index and cmdline of the current command into
- // the last_command_file if this command writes to the stash either explicitly of implicitly.
+ // Set up the new data writer.
+ if (params.canwrite) {
+ params.nti.za = za;
+ params.nti.entry = new_entry;
+ params.nti.brotli_compressed = android::base::EndsWith(new_data_fn->data, ".br");
+ if (params.nti.brotli_compressed) {
+ // Initialize brotli decoder state.
+ params.nti.brotli_decoder_state = BrotliDecoderCreateInstance(nullptr, nullptr, nullptr);
+ }
+ params.nti.receiver_available = true;
+
+ pthread_mutex_init(¶ms.nti.mu, nullptr);
+ pthread_cond_init(¶ms.nti.cv, nullptr);
+ pthread_attr_t attr;
+ pthread_attr_init(&attr);
+ pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
+
+ int error = pthread_create(¶ms.thread, &attr, unzip_new_data, ¶ms.nti);
+ if (error != 0) {
+ LOG(ERROR) << "pthread_create failed: " << strerror(error);
+ return StringValue("");
+ }
+ }
+
+ // When performing an update, save the index and cmdline of the current command into the
+ // last_command_file.
// Upon resuming an update, read the saved index first; then
// 1. In verification mode, check if the 'move' or 'diff' commands before the saved index has
// the expected target blocks already. If not, these commands cannot be skipped and we need
@@ -1684,90 +1761,86 @@
// 2. In update mode, skip all commands before the saved index. Therefore, we can avoid deleting
// stashes with duplicate id unintentionally (b/69858743); and also speed up the update.
// If an update succeeds or is unresumable, delete the last_command_file.
- int saved_last_command_index;
+ bool skip_executed_command = true;
+ size_t saved_last_command_index;
if (!ParseLastCommandFile(&saved_last_command_index)) {
DeleteLastCommandFile();
- // We failed to parse the last command, set it explicitly to -1.
- saved_last_command_index = -1;
- }
-
- start += 2;
-
- // Build a map of the available commands
- std::unordered_map<std::string, const Command*> cmd_map;
- for (size_t i = 0; i < cmdcount; ++i) {
- if (cmd_map.find(commands[i].name) != cmd_map.end()) {
- LOG(ERROR) << "Error: command [" << commands[i].name << "] already exists in the cmd map.";
- return StringValue(strdup(""));
- }
- cmd_map[commands[i].name] = &commands[i];
+ // We failed to parse the last command. Disallow skipping executed commands.
+ skip_executed_command = false;
}
int rc = -1;
// Subsequent lines are all individual transfer commands
- for (size_t i = start; i < lines.size(); i++) {
+ for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) {
const std::string& line = lines[i];
if (line.empty()) continue;
+ size_t cmdindex = i - kTransferListHeaderLines;
params.tokens = android::base::Split(line, " ");
params.cpos = 0;
- if (i - start > std::numeric_limits<int>::max()) {
- params.cmdindex = -1;
- } else {
- params.cmdindex = i - start;
- }
- params.cmdname = params.tokens[params.cpos++].c_str();
- params.cmdline = line.c_str();
+ params.cmdname = params.tokens[params.cpos++];
+ params.cmdline = line;
params.target_verified = false;
- if (cmd_map.find(params.cmdname) == cmd_map.end()) {
+ Command::Type cmd_type = Command::ParseType(params.cmdname);
+ if (cmd_type == Command::Type::LAST) {
LOG(ERROR) << "unexpected command [" << params.cmdname << "]";
goto pbiudone;
}
- const Command* cmd = cmd_map[params.cmdname];
+ const CommandFunction& performer = command_map.at(cmd_type);
// Skip the command if we explicitly set the corresponding function pointer to nullptr, e.g.
// "erase" during block_image_verify.
- if (cmd->f == nullptr) {
+ if (performer == nullptr) {
LOG(DEBUG) << "skip executing command [" << line << "]";
continue;
}
- // Skip all commands before the saved last command index when resuming an update.
- if (params.canwrite && params.cmdindex != -1 && params.cmdindex <= saved_last_command_index) {
- LOG(INFO) << "Skipping already executed command: " << params.cmdindex
+ // Skip all commands before the saved last command index when resuming an update, except for
+ // "new" command. Because new commands read in the data sequentially.
+ if (params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index &&
+ cmd_type != Command::Type::NEW) {
+ LOG(INFO) << "Skipping already executed command: " << cmdindex
<< ", last executed command for previous update: " << saved_last_command_index;
continue;
}
- if (cmd->f(params) == -1) {
+ if (performer(params) == -1) {
LOG(ERROR) << "failed to execute command [" << line << "]";
+ if (cmd_type == Command::Type::COMPUTE_HASH_TREE && failure_type == kNoCause) {
+ failure_type = kHashTreeComputationFailure;
+ }
goto pbiudone;
}
- // In verify mode, check if the commands before the saved last_command_index have been
- // executed correctly. If some target blocks have unexpected contents, delete the last command
- // file so that we will resume the update from the first command in the transfer list.
- if (!params.canwrite && saved_last_command_index != -1 && params.cmdindex != -1 &&
- params.cmdindex <= saved_last_command_index) {
+ // In verify mode, check if the commands before the saved last_command_index have been executed
+ // correctly. If some target blocks have unexpected contents, delete the last command file so
+ // that we will resume the update from the first command in the transfer list.
+ if (!params.canwrite && skip_executed_command && cmdindex <= saved_last_command_index) {
// TODO(xunchang) check that the cmdline of the saved index is correct.
- std::string cmdname = std::string(params.cmdname);
- if ((cmdname == "move" || cmdname == "bsdiff" || cmdname == "imgdiff") &&
+ if ((cmd_type == Command::Type::MOVE || cmd_type == Command::Type::BSDIFF ||
+ cmd_type == Command::Type::IMGDIFF) &&
!params.target_verified) {
LOG(WARNING) << "Previously executed command " << saved_last_command_index << ": "
<< params.cmdline << " doesn't produce expected target blocks.";
- saved_last_command_index = -1;
+ skip_executed_command = false;
DeleteLastCommandFile();
}
}
+
if (params.canwrite) {
- if (ota_fsync(params.fd) == -1) {
- failure_type = kFsyncFailure;
+ if (fsync(params.fd) == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
PLOG(ERROR) << "fsync failed";
goto pbiudone;
}
+
+ if (!UpdateLastCommandIndex(cmdindex, params.cmdline)) {
+ LOG(WARNING) << "Failed to update the last command file.";
+ }
+
fprintf(cmd_pipe, "set_progress %.4f\n", static_cast<double>(params.written) / total_blocks);
fflush(cmd_pipe);
}
@@ -1804,6 +1877,13 @@
// to complete the update later.
DeleteStash(params.stashbase);
DeleteLastCommandFile();
+
+ // Create a marker on /cache partition, which allows skipping the update on this partition on
+ // retry. The marker will be removed once booting into normal boot, or before starting next
+ // fresh install.
+ if (!SetPartitionUpdatedMarker(updated_marker)) {
+ LOG(WARNING) << "Failed to set updated marker; continuing";
+ }
}
pthread_mutex_destroy(¶ms.nti.mu);
@@ -1812,8 +1892,8 @@
LOG(INFO) << "verified partition contents; update may be resumed";
}
- if (ota_fsync(params.fd) == -1) {
- failure_type = kFsyncFailure;
+ if (fsync(params.fd) == -1) {
+ failure_type = errno == EIO ? kEioFailure : kFsyncFailure;
PLOG(ERROR) << "fsync failed";
}
// params.fd will be automatically closed because it's a unique_fd.
@@ -1886,38 +1966,46 @@
*/
Value* BlockImageVerifyFn(const char* name, State* state,
const std::vector<std::unique_ptr<Expr>>& argv) {
- // Commands which are not tested are set to nullptr to skip them completely
- const Command commands[] = {
- { "bsdiff", PerformCommandDiff },
- { "erase", nullptr },
- { "free", PerformCommandFree },
- { "imgdiff", PerformCommandDiff },
- { "move", PerformCommandMove },
- { "new", nullptr },
- { "stash", PerformCommandStash },
- { "zero", nullptr }
- };
+ // Commands which are not allowed are set to nullptr to skip them completely.
+ const CommandMap command_map{
+ // clang-format off
+ { Command::Type::ABORT, PerformCommandAbort },
+ { Command::Type::BSDIFF, PerformCommandDiff },
+ { Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
+ { Command::Type::ERASE, nullptr },
+ { Command::Type::FREE, PerformCommandFree },
+ { Command::Type::IMGDIFF, PerformCommandDiff },
+ { Command::Type::MOVE, PerformCommandMove },
+ { Command::Type::NEW, nullptr },
+ { Command::Type::STASH, PerformCommandStash },
+ { Command::Type::ZERO, nullptr },
+ // clang-format on
+ };
+ CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
- // Perform a dry run without writing to test if an update can proceed
- return PerformBlockImageUpdate(name, state, argv, commands,
- sizeof(commands) / sizeof(commands[0]), true);
+ // Perform a dry run without writing to test if an update can proceed.
+ return PerformBlockImageUpdate(name, state, argv, command_map, true);
}
Value* BlockImageUpdateFn(const char* name, State* state,
const std::vector<std::unique_ptr<Expr>>& argv) {
- const Command commands[] = {
- { "bsdiff", PerformCommandDiff },
- { "erase", PerformCommandErase },
- { "free", PerformCommandFree },
- { "imgdiff", PerformCommandDiff },
- { "move", PerformCommandMove },
- { "new", PerformCommandNew },
- { "stash", PerformCommandStash },
- { "zero", PerformCommandZero }
- };
+ const CommandMap command_map{
+ // clang-format off
+ { Command::Type::ABORT, PerformCommandAbort },
+ { Command::Type::BSDIFF, PerformCommandDiff },
+ { Command::Type::COMPUTE_HASH_TREE, PerformCommandComputeHashTree },
+ { Command::Type::ERASE, PerformCommandErase },
+ { Command::Type::FREE, PerformCommandFree },
+ { Command::Type::IMGDIFF, PerformCommandDiff },
+ { Command::Type::MOVE, PerformCommandMove },
+ { Command::Type::NEW, PerformCommandNew },
+ { Command::Type::STASH, PerformCommandStash },
+ { Command::Type::ZERO, PerformCommandZero },
+ // clang-format on
+ };
+ CHECK_EQ(static_cast<size_t>(Command::Type::LAST), command_map.size());
- return PerformBlockImageUpdate(name, state, argv, commands,
- sizeof(commands) / sizeof(commands[0]), false);
+ return PerformBlockImageUpdate(name, state, argv, command_map, false);
}
Value* RangeSha1Fn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
@@ -1934,18 +2022,19 @@
const std::unique_ptr<Value>& blockdev_filename = args[0];
const std::unique_ptr<Value>& ranges = args[1];
- if (blockdev_filename->type != VAL_STRING) {
+ if (blockdev_filename->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "blockdev_filename argument to %s must be string", name);
return StringValue("");
}
- if (ranges->type != VAL_STRING) {
+ if (ranges->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name);
return StringValue("");
}
- android::base::unique_fd fd(ota_open(blockdev_filename->data.c_str(), O_RDWR));
+ android::base::unique_fd fd(open(blockdev_filename->data.c_str(), O_RDWR));
if (fd == -1) {
- ErrorAbort(state, kFileOpenFailure, "open \"%s\" failed: %s", blockdev_filename->data.c_str(),
+ CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure;
+ ErrorAbort(state, cause_code, "open \"%s\" failed: %s", blockdev_filename->data.c_str(),
strerror(errno));
return StringValue("");
}
@@ -1957,16 +2046,17 @@
SHA1_Init(&ctx);
std::vector<uint8_t> buffer(BLOCKSIZE);
- for (const auto& range : rs) {
- if (!check_lseek(fd, static_cast<off64_t>(range.first) * BLOCKSIZE, SEEK_SET)) {
+ for (const auto& [begin, end] : rs) {
+ if (!check_lseek(fd, static_cast<off64_t>(begin) * BLOCKSIZE, SEEK_SET)) {
ErrorAbort(state, kLseekFailure, "failed to seek %s: %s", blockdev_filename->data.c_str(),
strerror(errno));
return StringValue("");
}
- for (size_t j = range.first; j < range.second; ++j) {
- if (read_all(fd, buffer, BLOCKSIZE) == -1) {
- ErrorAbort(state, kFreadFailure, "failed to read %s: %s", blockdev_filename->data.c_str(),
+ for (size_t j = begin; j < end; ++j) {
+ if (!android::base::ReadFully(fd, buffer.data(), BLOCKSIZE)) {
+ CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure;
+ ErrorAbort(state, cause_code, "failed to read %s: %s", blockdev_filename->data.c_str(),
strerror(errno));
return StringValue("");
}
@@ -2000,14 +2090,15 @@
const std::unique_ptr<Value>& arg_filename = args[0];
- if (arg_filename->type != VAL_STRING) {
+ if (arg_filename->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name);
return StringValue("");
}
- android::base::unique_fd fd(ota_open(arg_filename->data.c_str(), O_RDONLY));
+ android::base::unique_fd fd(open(arg_filename->data.c_str(), O_RDONLY));
if (fd == -1) {
- ErrorAbort(state, kFileOpenFailure, "open \"%s\" failed: %s", arg_filename->data.c_str(),
+ CauseCode cause_code = errno == EIO ? kEioFailure : kFileOpenFailure;
+ ErrorAbort(state, cause_code, "open \"%s\" failed: %s", arg_filename->data.c_str(),
strerror(errno));
return StringValue("");
}
@@ -2015,8 +2106,9 @@
RangeSet blk0(std::vector<Range>{ Range{ 0, 1 } });
std::vector<uint8_t> block0_buffer(BLOCKSIZE);
- if (ReadBlocks(blk0, block0_buffer, fd) == -1) {
- ErrorAbort(state, kFreadFailure, "failed to read %s: %s", arg_filename->data.c_str(),
+ if (ReadBlocks(blk0, &block0_buffer, fd) == -1) {
+ CauseCode cause_code = errno == EIO ? kEioFailure : kFreadFailure;
+ ErrorAbort(state, cause_code, "failed to read %s: %s", arg_filename->data.c_str(),
strerror(errno));
return StringValue("");
}
@@ -2055,11 +2147,11 @@
const std::unique_ptr<Value>& filename = args[0];
const std::unique_ptr<Value>& ranges = args[1];
- if (filename->type != VAL_STRING) {
+ if (filename->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "filename argument to %s must be string", name);
return StringValue("");
}
- if (ranges->type != VAL_STRING) {
+ if (ranges->type != Value::Type::STRING) {
ErrorAbort(state, kArgsParsingFailure, "ranges argument to %s must be string", name);
return StringValue("");
}
@@ -2093,8 +2185,8 @@
}
uint8_t buffer[BLOCKSIZE];
- for (const auto& range : rs) {
- for (size_t j = range.first; j < range.second; ++j) {
+ for (const auto& [begin, end] : rs) {
+ for (size_t j = begin; j < end; ++j) {
// Stay within the data area, libfec validates and corrects metadata
if (status.data_size <= static_cast<uint64_t>(j) * BLOCKSIZE) {
continue;
diff --git a/updater/commands.cpp b/updater/commands.cpp
new file mode 100644
index 0000000..aed6336
--- /dev/null
+++ b/updater/commands.cpp
@@ -0,0 +1,454 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "private/commands.h"
+
+#include <stdint.h>
+#include <string.h>
+
+#include <functional>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <android-base/logging.h>
+#include <android-base/parseint.h>
+#include <android-base/stringprintf.h>
+#include <android-base/strings.h>
+#include <openssl/sha.h>
+
+#include "otautil/print_sha1.h"
+#include "otautil/rangeset.h"
+
+using namespace std::string_literals;
+
+bool Command::abort_allowed_ = false;
+
+Command::Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info)
+ : type_(type),
+ index_(index),
+ cmdline_(std::move(cmdline)),
+ hash_tree_info_(std::move(hash_tree_info)) {
+ CHECK(type == Type::COMPUTE_HASH_TREE);
+}
+
+Command::Type Command::ParseType(const std::string& type_str) {
+ if (type_str == "abort") {
+ if (!abort_allowed_) {
+ LOG(ERROR) << "ABORT disallowed";
+ return Type::LAST;
+ }
+ return Type::ABORT;
+ } else if (type_str == "bsdiff") {
+ return Type::BSDIFF;
+ } else if (type_str == "compute_hash_tree") {
+ return Type::COMPUTE_HASH_TREE;
+ } else if (type_str == "erase") {
+ return Type::ERASE;
+ } else if (type_str == "free") {
+ return Type::FREE;
+ } else if (type_str == "imgdiff") {
+ return Type::IMGDIFF;
+ } else if (type_str == "move") {
+ return Type::MOVE;
+ } else if (type_str == "new") {
+ return Type::NEW;
+ } else if (type_str == "stash") {
+ return Type::STASH;
+ } else if (type_str == "zero") {
+ return Type::ZERO;
+ }
+ return Type::LAST;
+};
+
+bool Command::ParseTargetInfoAndSourceInfo(const std::vector<std::string>& tokens,
+ const std::string& tgt_hash, TargetInfo* target,
+ const std::string& src_hash, SourceInfo* source,
+ std::string* err) {
+ // We expect the given args (in 'tokens' vector) in one of the following formats.
+ //
+ // <tgt_ranges> <src_block_count> - <[stash_id:location] ...>
+ // (loads data from stashes only)
+ //
+ // <tgt_ranges> <src_block_count> <src_ranges>
+ // (loads data from source image only)
+ //
+ // <tgt_ranges> <src_block_count> <src_ranges> <src_ranges_location> <[stash_id:location] ...>
+ // (loads data from both of source image and stashes)
+
+ // At least it needs to provide three args: <tgt_ranges>, <src_block_count> and "-"/<src_ranges>.
+ if (tokens.size() < 3) {
+ *err = "invalid number of args";
+ return false;
+ }
+
+ size_t pos = 0;
+ RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!tgt_ranges) {
+ *err = "invalid target ranges";
+ return false;
+ }
+ *target = TargetInfo(tgt_hash, tgt_ranges);
+
+ // <src_block_count>
+ const std::string& token = tokens[pos++];
+ size_t src_blocks;
+ if (!android::base::ParseUint(token, &src_blocks)) {
+ *err = "invalid src_block_count \""s + token + "\"";
+ return false;
+ }
+
+ RangeSet src_ranges;
+ RangeSet src_ranges_location;
+ // "-" or <src_ranges> [<src_ranges_location>]
+ if (tokens[pos] == "-") {
+ // no source ranges, only stashes
+ pos++;
+ } else {
+ src_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!src_ranges) {
+ *err = "invalid source ranges";
+ return false;
+ }
+
+ if (pos >= tokens.size()) {
+ // No stashes, only source ranges.
+ SourceInfo result(src_hash, src_ranges, {}, {});
+
+ // Sanity check the block count.
+ if (result.blocks() != src_blocks) {
+ *err =
+ android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(),
+ src_ranges.ToString().c_str(), src_blocks);
+ return false;
+ }
+
+ *source = result;
+ return true;
+ }
+
+ src_ranges_location = RangeSet::Parse(tokens[pos++]);
+ if (!src_ranges_location) {
+ *err = "invalid source ranges location";
+ return false;
+ }
+ }
+
+ // <[stash_id:stash_location]>
+ std::vector<StashInfo> stashes;
+ while (pos < tokens.size()) {
+ // Each word is a an index into the stash table, a colon, and then a RangeSet describing where
+ // in the source block that stashed data should go.
+ std::vector<std::string> pairs = android::base::Split(tokens[pos++], ":");
+ if (pairs.size() != 2) {
+ *err = "invalid stash info";
+ return false;
+ }
+ RangeSet stash_location = RangeSet::Parse(pairs[1]);
+ if (!stash_location) {
+ *err = "invalid stash location";
+ return false;
+ }
+ stashes.emplace_back(pairs[0], stash_location);
+ }
+
+ SourceInfo result(src_hash, src_ranges, src_ranges_location, stashes);
+ if (src_blocks != result.blocks()) {
+ *err = android::base::StringPrintf("mismatching block count: %zu (%s) vs %zu", result.blocks(),
+ src_ranges.ToString().c_str(), src_blocks);
+ return false;
+ }
+
+ *source = result;
+ return true;
+}
+
+Command Command::Parse(const std::string& line, size_t index, std::string* err) {
+ std::vector<std::string> tokens = android::base::Split(line, " ");
+ size_t pos = 0;
+ // tokens.size() will be 1 at least.
+ Type op = ParseType(tokens[pos++]);
+ if (op == Type::LAST) {
+ *err = "invalid type";
+ return {};
+ }
+
+ PatchInfo patch_info;
+ TargetInfo target_info;
+ SourceInfo source_info;
+ StashInfo stash_info;
+
+ if (op == Type::ZERO || op == Type::NEW || op == Type::ERASE) {
+ // zero/new/erase <rangeset>
+ if (pos + 1 != tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)",
+ tokens.size() - pos);
+ return {};
+ }
+ RangeSet tgt_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!tgt_ranges) {
+ return {};
+ }
+ static const std::string kUnknownHash{ "unknown-hash" };
+ target_info = TargetInfo(kUnknownHash, tgt_ranges);
+ } else if (op == Type::STASH) {
+ // stash <stash_id> <src_ranges>
+ if (pos + 2 != tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 2)",
+ tokens.size() - pos);
+ return {};
+ }
+ const std::string& id = tokens[pos++];
+ RangeSet src_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!src_ranges) {
+ *err = "invalid token";
+ return {};
+ }
+ stash_info = StashInfo(id, src_ranges);
+ } else if (op == Type::FREE) {
+ // free <stash_id>
+ if (pos + 1 != tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 1)",
+ tokens.size() - pos);
+ return {};
+ }
+ stash_info = StashInfo(tokens[pos++], {});
+ } else if (op == Type::MOVE) {
+ // <hash>
+ if (pos + 1 > tokens.size()) {
+ *err = "missing hash";
+ return {};
+ }
+ std::string hash = tokens[pos++];
+ if (!ParseTargetInfoAndSourceInfo(
+ std::vector<std::string>(tokens.cbegin() + pos, tokens.cend()), hash, &target_info,
+ hash, &source_info, err)) {
+ return {};
+ }
+ } else if (op == Type::BSDIFF || op == Type::IMGDIFF) {
+ // <offset> <length> <srchash> <dsthash>
+ if (pos + 4 > tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 4+)",
+ tokens.size() - pos);
+ return {};
+ }
+ size_t offset;
+ size_t length;
+ if (!android::base::ParseUint(tokens[pos++], &offset) ||
+ !android::base::ParseUint(tokens[pos++], &length)) {
+ *err = "invalid patch offset/length";
+ return {};
+ }
+ patch_info = PatchInfo(offset, length);
+
+ std::string src_hash = tokens[pos++];
+ std::string dst_hash = tokens[pos++];
+ if (!ParseTargetInfoAndSourceInfo(
+ std::vector<std::string>(tokens.cbegin() + pos, tokens.cend()), dst_hash, &target_info,
+ src_hash, &source_info, err)) {
+ return {};
+ }
+ } else if (op == Type::ABORT) {
+ // No-op, other than sanity checking the input args.
+ if (pos != tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 0)",
+ tokens.size() - pos);
+ return {};
+ }
+ } else if (op == Type::COMPUTE_HASH_TREE) {
+ // <hash_tree_ranges> <source_ranges> <hash_algorithm> <salt_hex> <root_hash>
+ if (pos + 5 != tokens.size()) {
+ *err = android::base::StringPrintf("invalid number of args: %zu (expected 5)",
+ tokens.size() - pos);
+ return {};
+ }
+
+ // Expects the hash_tree data to be contiguous.
+ RangeSet hash_tree_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!hash_tree_ranges || hash_tree_ranges.size() != 1) {
+ *err = "invalid hash tree ranges in: " + line;
+ return {};
+ }
+
+ RangeSet source_ranges = RangeSet::Parse(tokens[pos++]);
+ if (!source_ranges) {
+ *err = "invalid source ranges in: " + line;
+ return {};
+ }
+
+ std::string hash_algorithm = tokens[pos++];
+ std::string salt_hex = tokens[pos++];
+ std::string root_hash = tokens[pos++];
+ if (hash_algorithm.empty() || salt_hex.empty() || root_hash.empty()) {
+ *err = "invalid hash tree arguments in " + line;
+ return {};
+ }
+
+ HashTreeInfo hash_tree_info(std::move(hash_tree_ranges), std::move(source_ranges),
+ std::move(hash_algorithm), std::move(salt_hex),
+ std::move(root_hash));
+ return Command(op, index, line, std::move(hash_tree_info));
+ } else {
+ *err = "invalid op";
+ return {};
+ }
+
+ return Command(op, index, line, patch_info, target_info, source_info, stash_info);
+}
+
+bool SourceInfo::Overlaps(const TargetInfo& target) const {
+ return ranges_.Overlaps(target.ranges());
+}
+
+// Moves blocks in the 'source' vector to the specified locations (as in 'locs') in the 'dest'
+// vector. Note that source and dest may be the same buffer.
+static void MoveRange(std::vector<uint8_t>* dest, const RangeSet& locs,
+ const std::vector<uint8_t>& source, size_t block_size) {
+ const uint8_t* from = source.data();
+ uint8_t* to = dest->data();
+ size_t start = locs.blocks();
+ // Must do the movement backward.
+ for (auto it = locs.crbegin(); it != locs.crend(); it++) {
+ size_t blocks = it->second - it->first;
+ start -= blocks;
+ memmove(to + (it->first * block_size), from + (start * block_size), blocks * block_size);
+ }
+}
+
+bool SourceInfo::ReadAll(
+ std::vector<uint8_t>* buffer, size_t block_size,
+ const std::function<int(const RangeSet&, std::vector<uint8_t>*)>& block_reader,
+ const std::function<int(const std::string&, std::vector<uint8_t>*)>& stash_reader) const {
+ if (buffer->size() < blocks() * block_size) {
+ return false;
+ }
+
+ // Read in the source ranges.
+ if (ranges_) {
+ if (block_reader(ranges_, buffer) != 0) {
+ return false;
+ }
+ if (location_) {
+ MoveRange(buffer, location_, *buffer, block_size);
+ }
+ }
+
+ // Read in the stashes.
+ for (const StashInfo& stash : stashes_) {
+ std::vector<uint8_t> stash_buffer(stash.blocks() * block_size);
+ if (stash_reader(stash.id(), &stash_buffer) != 0) {
+ return false;
+ }
+ MoveRange(buffer, stash.ranges(), stash_buffer, block_size);
+ }
+ return true;
+}
+
+void SourceInfo::DumpBuffer(const std::vector<uint8_t>& buffer, size_t block_size) const {
+ LOG(INFO) << "Dumping hashes in hex for " << ranges_.blocks() << " source blocks";
+
+ const RangeSet& location = location_ ? location_ : RangeSet({ Range{ 0, ranges_.blocks() } });
+ for (size_t i = 0; i < ranges_.blocks(); i++) {
+ size_t block_num = ranges_.GetBlockNumber(i);
+ size_t buffer_index = location.GetBlockNumber(i);
+ CHECK_LE((buffer_index + 1) * block_size, buffer.size());
+
+ uint8_t digest[SHA_DIGEST_LENGTH];
+ SHA1(buffer.data() + buffer_index * block_size, block_size, digest);
+ std::string hexdigest = print_sha1(digest);
+ LOG(INFO) << " block number: " << block_num << ", SHA-1: " << hexdigest;
+ }
+}
+
+std::ostream& operator<<(std::ostream& os, const Command& command) {
+ os << command.index() << ": " << command.cmdline();
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const TargetInfo& target) {
+ os << target.blocks() << " blocks (" << target.hash_ << "): " << target.ranges_.ToString();
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const StashInfo& stash) {
+ os << stash.blocks() << " blocks (" << stash.id_ << "): " << stash.ranges_.ToString();
+ return os;
+}
+
+std::ostream& operator<<(std::ostream& os, const SourceInfo& source) {
+ os << source.blocks_ << " blocks (" << source.hash_ << "): ";
+ if (source.ranges_) {
+ os << source.ranges_.ToString();
+ if (source.location_) {
+ os << " (location: " << source.location_.ToString() << ")";
+ }
+ }
+ if (!source.stashes_.empty()) {
+ os << " " << source.stashes_.size() << " stash(es)";
+ }
+ return os;
+}
+
+TransferList TransferList::Parse(const std::string& transfer_list_str, std::string* err) {
+ TransferList result{};
+
+ std::vector<std::string> lines = android::base::Split(transfer_list_str, "\n");
+ if (lines.size() < kTransferListHeaderLines) {
+ *err = android::base::StringPrintf("too few lines in the transfer list [%zu]", lines.size());
+ return TransferList{};
+ }
+
+ // First line in transfer list is the version number.
+ if (!android::base::ParseInt(lines[0], &result.version_, 3, 4)) {
+ *err = "unexpected transfer list version ["s + lines[0] + "]";
+ return TransferList{};
+ }
+
+ // Second line in transfer list is the total number of blocks we expect to write.
+ if (!android::base::ParseUint(lines[1], &result.total_blocks_)) {
+ *err = "unexpected block count ["s + lines[1] + "]";
+ return TransferList{};
+ }
+
+ // Third line is how many stash entries are needed simultaneously.
+ if (!android::base::ParseUint(lines[2], &result.stash_max_entries_)) {
+ return TransferList{};
+ }
+
+ // Fourth line is the maximum number of blocks that will be stashed simultaneously.
+ if (!android::base::ParseUint(lines[3], &result.stash_max_blocks_)) {
+ *err = "unexpected maximum stash blocks ["s + lines[3] + "]";
+ return TransferList{};
+ }
+
+ // Subsequent lines are all individual transfer commands.
+ for (size_t i = kTransferListHeaderLines; i < lines.size(); i++) {
+ const std::string& line = lines[i];
+ if (line.empty()) continue;
+
+ size_t cmdindex = i - kTransferListHeaderLines;
+ std::string parsing_error;
+ Command command = Command::Parse(line, cmdindex, &parsing_error);
+ if (!command) {
+ *err = android::base::StringPrintf("Failed to parse command %zu [%s]: %s", cmdindex,
+ line.c_str(), parsing_error.c_str());
+ return TransferList{};
+ }
+ result.commands_.push_back(command);
+ }
+
+ return result;
+}
diff --git a/updater/include/private/commands.h b/updater/include/private/commands.h
new file mode 100644
index 0000000..79f9154
--- /dev/null
+++ b/updater/include/private/commands.h
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <functional>
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest_prod.h> // FRIEND_TEST
+
+#include "otautil/rangeset.h"
+
+// Represents the target info used in a Command. TargetInfo contains the ranges of the blocks and
+// the expected hash.
+class TargetInfo {
+ public:
+ TargetInfo() = default;
+
+ TargetInfo(std::string hash, RangeSet ranges)
+ : hash_(std::move(hash)), ranges_(std::move(ranges)) {}
+
+ const std::string& hash() const {
+ return hash_;
+ }
+
+ const RangeSet& ranges() const {
+ return ranges_;
+ }
+
+ size_t blocks() const {
+ return ranges_.blocks();
+ }
+
+ bool operator==(const TargetInfo& other) const {
+ return hash_ == other.hash_ && ranges_ == other.ranges_;
+ }
+
+ private:
+ friend std::ostream& operator<<(std::ostream& os, const TargetInfo& source);
+
+ // The hash of the data represented by the object.
+ std::string hash_;
+ // The block ranges that the data should be written to.
+ RangeSet ranges_;
+};
+
+std::ostream& operator<<(std::ostream& os, const TargetInfo& source);
+
+// Represents the stash info used in a Command.
+class StashInfo {
+ public:
+ StashInfo() = default;
+
+ StashInfo(std::string id, RangeSet ranges) : id_(std::move(id)), ranges_(std::move(ranges)) {}
+
+ size_t blocks() const {
+ return ranges_.blocks();
+ }
+
+ const std::string& id() const {
+ return id_;
+ }
+
+ const RangeSet& ranges() const {
+ return ranges_;
+ }
+
+ bool operator==(const StashInfo& other) const {
+ return id_ == other.id_ && ranges_ == other.ranges_;
+ }
+
+ private:
+ friend std::ostream& operator<<(std::ostream& os, const StashInfo& stash);
+
+ // The id (i.e. hash) of the stash.
+ std::string id_;
+ // The matching location of the stash.
+ RangeSet ranges_;
+};
+
+std::ostream& operator<<(std::ostream& os, const StashInfo& stash);
+
+// Represents the source info in a Command, whose data could come from source image, stashed blocks,
+// or both.
+class SourceInfo {
+ public:
+ SourceInfo() = default;
+
+ SourceInfo(std::string hash, RangeSet ranges, RangeSet location, std::vector<StashInfo> stashes)
+ : hash_(std::move(hash)),
+ ranges_(std::move(ranges)),
+ location_(std::move(location)),
+ stashes_(std::move(stashes)) {
+ blocks_ = ranges_.blocks();
+ for (const auto& stash : stashes_) {
+ blocks_ += stash.ranges().blocks();
+ }
+ }
+
+ // Reads all the data specified by this SourceInfo object into the given 'buffer', by calling the
+ // given readers. Caller needs to specify the block size for the represented blocks. The given
+ // buffer needs to be sufficiently large. Otherwise it returns false. 'block_reader' and
+ // 'stash_reader' read the specified data into the given buffer (guaranteed to be large enough)
+ // respectively. The readers should return 0 on success, or -1 on error.
+ bool ReadAll(
+ std::vector<uint8_t>* buffer, size_t block_size,
+ const std::function<int(const RangeSet&, std::vector<uint8_t>*)>& block_reader,
+ const std::function<int(const std::string&, std::vector<uint8_t>*)>& stash_reader) const;
+
+ // Whether this SourceInfo overlaps with the given TargetInfo object.
+ bool Overlaps(const TargetInfo& target) const;
+
+ // Dumps the hashes in hex for the given buffer that's loaded from this SourceInfo object
+ // (excluding the stashed blocks which are handled separately).
+ void DumpBuffer(const std::vector<uint8_t>& buffer, size_t block_size) const;
+
+ const std::string& hash() const {
+ return hash_;
+ }
+
+ size_t blocks() const {
+ return blocks_;
+ }
+
+ bool operator==(const SourceInfo& other) const {
+ return hash_ == other.hash_ && ranges_ == other.ranges_ && location_ == other.location_ &&
+ stashes_ == other.stashes_;
+ }
+
+ private:
+ friend std::ostream& operator<<(std::ostream& os, const SourceInfo& source);
+
+ // The hash of the data represented by the object.
+ std::string hash_;
+ // The block ranges from the source image to read data from. This could be a subset of all the
+ // blocks represented by the object, or empty if all the data should be loaded from stash.
+ RangeSet ranges_;
+ // The location in the buffer to load ranges_ into. Empty if ranges_ alone covers all the blocks
+ // (i.e. nothing needs to be loaded from stash).
+ RangeSet location_;
+ // The info for the stashed blocks that are part of the source. Empty if there's none.
+ std::vector<StashInfo> stashes_;
+ // Total number of blocks represented by the object.
+ size_t blocks_{ 0 };
+};
+
+std::ostream& operator<<(std::ostream& os, const SourceInfo& source);
+
+class PatchInfo {
+ public:
+ PatchInfo() = default;
+
+ PatchInfo(size_t offset, size_t length) : offset_(offset), length_(length) {}
+
+ size_t offset() const {
+ return offset_;
+ }
+
+ size_t length() const {
+ return length_;
+ }
+
+ bool operator==(const PatchInfo& other) const {
+ return offset_ == other.offset_ && length_ == other.length_;
+ }
+
+ private:
+ size_t offset_{ 0 };
+ size_t length_{ 0 };
+};
+
+// The arguments to build a hash tree from blocks on the block device.
+class HashTreeInfo {
+ public:
+ HashTreeInfo() = default;
+
+ HashTreeInfo(RangeSet hash_tree_ranges, RangeSet source_ranges, std::string hash_algorithm,
+ std::string salt_hex, std::string root_hash)
+ : hash_tree_ranges_(std::move(hash_tree_ranges)),
+ source_ranges_(std::move(source_ranges)),
+ hash_algorithm_(std::move(hash_algorithm)),
+ salt_hex_(std::move(salt_hex)),
+ root_hash_(std::move(root_hash)) {}
+
+ const RangeSet& hash_tree_ranges() const {
+ return hash_tree_ranges_;
+ }
+ const RangeSet& source_ranges() const {
+ return source_ranges_;
+ }
+
+ const std::string& hash_algorithm() const {
+ return hash_algorithm_;
+ }
+ const std::string& salt_hex() const {
+ return salt_hex_;
+ }
+ const std::string& root_hash() const {
+ return root_hash_;
+ }
+
+ bool operator==(const HashTreeInfo& other) const {
+ return hash_tree_ranges_ == other.hash_tree_ranges_ && source_ranges_ == other.source_ranges_ &&
+ hash_algorithm_ == other.hash_algorithm_ && salt_hex_ == other.salt_hex_ &&
+ root_hash_ == other.root_hash_;
+ }
+
+ private:
+ RangeSet hash_tree_ranges_;
+ RangeSet source_ranges_;
+ std::string hash_algorithm_;
+ std::string salt_hex_;
+ std::string root_hash_;
+};
+
+// Command class holds the info for an update command that performs block-based OTA (BBOTA). Each
+// command consists of one or several args, namely TargetInfo, SourceInfo, StashInfo and PatchInfo.
+// The currently used BBOTA version is v4.
+//
+// zero <tgt_ranges>
+// - Fill the indicated blocks with zeros.
+// - Meaningful args: TargetInfo
+//
+// new <tgt_ranges>
+// - Fill the blocks with data read from the new_data file.
+// - Meaningful args: TargetInfo
+//
+// erase <tgt_ranges>
+// - Mark the given blocks as empty.
+// - Meaningful args: TargetInfo
+//
+// move <hash> <...>
+// - Read the source blocks, write result to target blocks.
+// - Meaningful args: TargetInfo, SourceInfo
+//
+// See the note below for <...>.
+//
+// bsdiff <patchstart> <patchlen> <srchash> <dsthash> <...>
+// imgdiff <patchstart> <patchlen> <srchash> <dsthash> <...>
+// - Read the source blocks, apply a patch, and write result to target blocks.
+// - Meaningful args: PatchInfo, TargetInfo, SourceInfo
+//
+// It expects <...> in one of the following formats:
+//
+// <tgt_ranges> <src_block_count> - <[stash_id:stash_location] ...>
+// (loads data from stashes only)
+//
+// <tgt_ranges> <src_block_count> <src_ranges>
+// (loads data from source image only)
+//
+// <tgt_ranges> <src_block_count> <src_ranges> <src_ranges_location>
+// <[stash_id:stash_location] ...>
+// (loads data from both of source image and stashes)
+//
+// stash <stash_id> <src_ranges>
+// - Load the given source blocks and stash the data in the given slot of the stash table.
+// - Meaningful args: StashInfo
+//
+// free <stash_id>
+// - Free the given stash data.
+// - Meaningful args: StashInfo
+//
+// compute_hash_tree <hash_tree_ranges> <source_ranges> <hash_algorithm> <salt_hex> <root_hash>
+// - Computes the hash_tree bytes and writes the result to the specified range on the
+// block_device.
+//
+// abort
+// - Abort the current update. Allowed for testing code only.
+//
+class Command {
+ public:
+ enum class Type {
+ ABORT,
+ BSDIFF,
+ COMPUTE_HASH_TREE,
+ ERASE,
+ FREE,
+ IMGDIFF,
+ MOVE,
+ NEW,
+ STASH,
+ ZERO,
+ LAST, // Not a valid type.
+ };
+
+ Command() = default;
+
+ Command(Type type, size_t index, std::string cmdline, PatchInfo patch, TargetInfo target,
+ SourceInfo source, StashInfo stash)
+ : type_(type),
+ index_(index),
+ cmdline_(std::move(cmdline)),
+ patch_(std::move(patch)),
+ target_(std::move(target)),
+ source_(std::move(source)),
+ stash_(std::move(stash)) {}
+
+ Command(Type type, size_t index, std::string cmdline, HashTreeInfo hash_tree_info);
+
+ // Parses the given command 'line' into a Command object and returns it. The 'index' is specified
+ // by the caller to index the object. On parsing error, it returns an empty Command object that
+ // evaluates to false, and the specific error message will be set in 'err'.
+ static Command Parse(const std::string& line, size_t index, std::string* err);
+
+ // Parses the command type from the given string.
+ static Type ParseType(const std::string& type_str);
+
+ Type type() const {
+ return type_;
+ }
+
+ size_t index() const {
+ return index_;
+ }
+
+ const std::string& cmdline() const {
+ return cmdline_;
+ }
+
+ const PatchInfo& patch() const {
+ return patch_;
+ }
+
+ const TargetInfo& target() const {
+ return target_;
+ }
+
+ const SourceInfo& source() const {
+ return source_;
+ }
+
+ const StashInfo& stash() const {
+ return stash_;
+ }
+
+ const HashTreeInfo& hash_tree_info() const {
+ return hash_tree_info_;
+ }
+
+ size_t block_size() const {
+ return block_size_;
+ }
+
+ constexpr explicit operator bool() const {
+ return type_ != Type::LAST;
+ }
+
+ private:
+ friend class ResumableUpdaterTest;
+ friend class UpdaterTest;
+
+ FRIEND_TEST(CommandsTest, Parse_ABORT_Allowed);
+ FRIEND_TEST(CommandsTest, Parse_InvalidNumberOfArgs);
+ FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_InvalidInput);
+ FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_StashesOnly);
+ FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksAndStashes);
+ FRIEND_TEST(CommandsTest, ParseTargetInfoAndSourceInfo_SourceBlocksOnly);
+
+ // Parses the target and source info from the given 'tokens' vector. Saves the parsed info into
+ // 'target' and 'source' objects. Returns the parsing result. Error message will be set in 'err'
+ // on parsing error, and the contents in 'target' and 'source' will be undefined.
+ static bool ParseTargetInfoAndSourceInfo(const std::vector<std::string>& tokens,
+ const std::string& tgt_hash, TargetInfo* target,
+ const std::string& src_hash, SourceInfo* source,
+ std::string* err);
+
+ // Allows parsing ABORT command, which should be used for testing purpose only.
+ static bool abort_allowed_;
+
+ // The type of the command.
+ Type type_{ Type::LAST };
+ // The index of the Command object, which is specified by the caller.
+ size_t index_{ 0 };
+ // The input string that the Command object is parsed from.
+ std::string cmdline_;
+ // The patch info. Only meaningful for BSDIFF and IMGDIFF commands.
+ PatchInfo patch_;
+ // The target info, where the command should be written to.
+ TargetInfo target_;
+ // The source info to load the source blocks for the command.
+ SourceInfo source_;
+ // The stash info. Only meaningful for STASH and FREE commands. Note that although SourceInfo may
+ // also load data from stash, such info will be owned and managed by SourceInfo (i.e. in source_).
+ StashInfo stash_;
+ // The hash_tree info. Only meaningful for COMPUTE_HASH_TREE.
+ HashTreeInfo hash_tree_info_;
+ // The unit size of each block to be used in this command.
+ size_t block_size_{ 4096 };
+};
+
+std::ostream& operator<<(std::ostream& os, const Command& command);
+
+// TransferList represents the info for a transfer list, which is parsed from input text lines
+// containing commands to transfer data from one place to another on the target partition.
+//
+// The creator of the transfer list will guarantee that no block is read (i.e., used as the source
+// for a patch or move) after it has been written.
+//
+// The creator will guarantee that a given stash is loaded (with a stash command) before it's used
+// in a move/bsdiff/imgdiff command.
+//
+// Within one command the source and target ranges may overlap so in general we need to read the
+// entire source into memory before writing anything to the target blocks.
+//
+// All the patch data is concatenated into one patch_data file in the update package. It must be
+// stored uncompressed because we memory-map it in directly from the archive. (Since patches are
+// already compressed, we lose very little by not compressing their concatenation.)
+//
+// Commands that read data from the partition (i.e. move/bsdiff/imgdiff/stash) have one or more
+// additional hashes before the range parameters, which are used to check if the command has
+// already been completed and verify the integrity of the source data.
+class TransferList {
+ public:
+ // Number of header lines.
+ static constexpr size_t kTransferListHeaderLines = 4;
+
+ TransferList() = default;
+
+ // Parses the given input string and returns a TransferList object. Sets error message if any.
+ static TransferList Parse(const std::string& transfer_list_str, std::string* err);
+
+ int version() const {
+ return version_;
+ }
+
+ size_t total_blocks() const {
+ return total_blocks_;
+ }
+
+ size_t stash_max_entries() const {
+ return stash_max_entries_;
+ }
+
+ size_t stash_max_blocks() const {
+ return stash_max_blocks_;
+ }
+
+ const std::vector<Command>& commands() const {
+ return commands_;
+ }
+
+ // Returns whether the TransferList is valid.
+ constexpr explicit operator bool() const {
+ return version_ != 0;
+ }
+
+ private:
+ // BBOTA version.
+ int version_{ 0 };
+ // Total number of blocks to be written in this transfer.
+ size_t total_blocks_;
+ // Maximum number of stashes that exist at the same time.
+ size_t stash_max_entries_;
+ // Maximum number of blocks to be stashed.
+ size_t stash_max_blocks_;
+ // Commands in this transfer.
+ std::vector<Command> commands_;
+};
diff --git a/updater/install.cpp b/updater/install.cpp
index 9be7645..deb7a2b 100644
--- a/updater/install.cpp
+++ b/updater/install.cpp
@@ -46,9 +46,9 @@
#include <android-base/properties.h>
#include <android-base/stringprintf.h>
#include <android-base/strings.h>
+#include <android-base/unique_fd.h>
#include <applypatch/applypatch.h>
#include <bootloader_message/bootloader_message.h>
-#include <cutils/android_reboot.h>
#include <ext4_utils/wipe.h>
#include <openssl/sha.h>
#include <selinux/label.h>
@@ -57,11 +57,11 @@
#include <ziparchive/zip_archive.h>
#include "edify/expr.h"
-#include "mounts.h"
-#include "otafault/ota_io.h"
-#include "otautil/DirUtil.h"
+#include "otautil/dirutil.h"
#include "otautil/error_code.h"
+#include "otautil/mounts.h"
#include "otautil/print_sha1.h"
+#include "otautil/sysutil.h"
#include "updater/updater.h"
// Send over the buffer to recovery though the command pipe.
@@ -137,8 +137,8 @@
return StringValue("");
}
- unique_fd fd(TEMP_FAILURE_RETRY(
- ota_open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)));
+ android::base::unique_fd fd(TEMP_FAILURE_RETRY(
+ open(dest_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR)));
if (fd == -1) {
PLOG(ERROR) << name << ": can't open " << dest_path << " for write";
return StringValue("");
@@ -152,11 +152,12 @@
<< "\": " << ErrorCodeString(ret);
success = false;
}
- if (ota_fsync(fd) == -1) {
+ if (fsync(fd) == -1) {
PLOG(ERROR) << "fsync of \"" << dest_path << "\" failed";
success = false;
}
- if (ota_close(fd) == -1) {
+
+ if (close(fd.release()) != 0) {
PLOG(ERROR) << "close of \"" << dest_path << "\" failed";
success = false;
}
@@ -191,145 +192,86 @@
zip_path.c_str(), buffer.size(), ErrorCodeString(ret));
}
- return new Value(VAL_BLOB, buffer);
+ return new Value(Value::Type::BLOB, buffer);
}
}
-// apply_patch(src_file, tgt_file, tgt_sha1, tgt_size, patch1_sha1, patch1_blob, [...])
-// Applies a binary patch to the src_file to produce the tgt_file. If the desired target is the
-// same as the source, pass "-" for tgt_file. tgt_sha1 and tgt_size are the expected final SHA1
-// hash and size of the target file. The remaining arguments must come in pairs: a SHA1 hash (a
-// 40-character hex string) and a blob. The blob is the patch to be applied when the source
-// file's current contents have the given SHA1.
+// patch_partition_check(target_partition, source_partition)
+// Checks if the target and source partitions have the desired checksums to be patched. It returns
+// directly, if the target partition already has the expected checksum. Otherwise it in turn
+// checks the integrity of the source partition and the backup file on /cache.
//
-// The patching is done in a safe manner that guarantees the target file either has the desired
-// SHA1 hash and size, or it is untouched -- it will not be left in an unrecoverable intermediate
-// state. If the process is interrupted during patching, the target file may be in an intermediate
-// state; a copy exists in the cache partition so restarting the update can successfully update
-// the file.
-Value* ApplyPatchFn(const char* name, State* state,
- const std::vector<std::unique_ptr<Expr>>& argv) {
- if (argv.size() < 6 || (argv.size() % 2) == 1) {
+// For example, patch_partition_check(
+// "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d",
+// "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4")
+Value* PatchPartitionCheckFn(const char* name, State* state,
+ const std::vector<std::unique_ptr<Expr>>& argv) {
+ if (argv.size() != 2) {
return ErrorAbort(state, kArgsParsingFailure,
- "%s(): expected at least 6 args and an "
- "even number, got %zu",
- name, argv.size());
+ "%s(): Invalid number of args (expected 2, got %zu)", name, argv.size());
}
std::vector<std::string> args;
- if (!ReadArgs(state, argv, &args, 0, 4)) {
- return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
- }
- const std::string& source_filename = args[0];
- const std::string& target_filename = args[1];
- const std::string& target_sha1 = args[2];
- const std::string& target_size_str = args[3];
-
- size_t target_size;
- if (!android::base::ParseUint(target_size_str.c_str(), &target_size)) {
- return ErrorAbort(state, kArgsParsingFailure, "%s(): can't parse \"%s\" as byte count", name,
- target_size_str.c_str());
+ if (!ReadArgs(state, argv, &args, 0, 2)) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
}
- int patchcount = (argv.size() - 4) / 2;
- std::vector<std::unique_ptr<Value>> arg_values;
- if (!ReadValueArgs(state, argv, &arg_values, 4, argv.size() - 4)) {
- return nullptr;
+ std::string err;
+ auto target = Partition::Parse(args[0], &err);
+ if (!target) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name,
+ args[0].c_str(), err.c_str());
}
- for (int i = 0; i < patchcount; ++i) {
- if (arg_values[i * 2]->type != VAL_STRING) {
- return ErrorAbort(state, kArgsParsingFailure, "%s(): sha-1 #%d is not string", name, i * 2);
- }
- if (arg_values[i * 2 + 1]->type != VAL_BLOB) {
- return ErrorAbort(state, kArgsParsingFailure, "%s(): patch #%d is not blob", name, i * 2 + 1);
- }
+ auto source = Partition::Parse(args[1], &err);
+ if (!source) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name,
+ args[1].c_str(), err.c_str());
}
- std::vector<std::string> patch_sha_str;
- std::vector<std::unique_ptr<Value>> patches;
- for (int i = 0; i < patchcount; ++i) {
- patch_sha_str.push_back(arg_values[i * 2]->data);
- patches.push_back(std::move(arg_values[i * 2 + 1]));
- }
-
- int result = applypatch(source_filename.c_str(), target_filename.c_str(), target_sha1.c_str(),
- target_size, patch_sha_str, patches, nullptr);
-
- return StringValue(result == 0 ? "t" : "");
+ bool result = PatchPartitionCheck(target, source);
+ return StringValue(result ? "t" : "");
}
-// apply_patch_check(filename, [sha1, ...])
-// Returns true if the contents of filename or the temporary copy in the cache partition (if
-// present) have a SHA-1 checksum equal to one of the given sha1 values. sha1 values are
-// specified as 40 hex digits. This function differs from sha1_check(read_file(filename),
-// sha1 [, ...]) in that it knows to check the cache partition copy, so apply_patch_check() will
-// succeed even if the file was corrupted by an interrupted apply_patch() update.
-Value* ApplyPatchCheckFn(const char* name, State* state,
- const std::vector<std::unique_ptr<Expr>>& argv) {
- if (argv.size() < 1) {
- return ErrorAbort(state, kArgsParsingFailure, "%s(): expected at least 1 arg, got %zu", name,
- argv.size());
+// patch_partition(target, source, patch)
+// Applies the given patch to the source partition, and writes the result to the target partition.
+//
+// For example, patch_partition(
+// "EMMC:/dev/block/boot:12342568:8aaacf187a6929d0e9c3e9e46ea7ff495b43424d",
+// "EMMC:/dev/block/boot:12363048:06b0b16299dcefc94900efed01e0763ff644ffa4",
+// package_extract_file("boot.img.p"))
+Value* PatchPartitionFn(const char* name, State* state,
+ const std::vector<std::unique_ptr<Expr>>& argv) {
+ if (argv.size() != 3) {
+ return ErrorAbort(state, kArgsParsingFailure,
+ "%s(): Invalid number of args (expected 3, got %zu)", name, argv.size());
}
std::vector<std::string> args;
- if (!ReadArgs(state, argv, &args, 0, 1)) {
- return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
- }
- const std::string& filename = args[0];
-
- std::vector<std::string> sha1s;
- if (argv.size() > 1 && !ReadArgs(state, argv, &sha1s, 1, argv.size() - 1)) {
- return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
- }
- int result = applypatch_check(filename.c_str(), sha1s);
-
- return StringValue(result == 0 ? "t" : "");
-}
-
-// sha1_check(data)
-// to return the sha1 of the data (given in the format returned by
-// read_file).
-//
-// sha1_check(data, sha1_hex, [sha1_hex, ...])
-// returns the sha1 of the file if it matches any of the hex
-// strings passed, or "" if it does not equal any of them.
-//
-Value* Sha1CheckFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
- if (argv.size() < 1) {
- return ErrorAbort(state, kArgsParsingFailure, "%s() expects at least 1 arg", name);
+ if (!ReadArgs(state, argv, &args, 0, 2)) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
}
- std::vector<std::unique_ptr<Value>> args;
- if (!ReadValueArgs(state, argv, &args)) {
- return nullptr;
+ std::string err;
+ auto target = Partition::Parse(args[0], &err);
+ if (!target) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse target \"%s\": %s", name,
+ args[0].c_str(), err.c_str());
}
- if (args[0]->type == VAL_INVALID) {
- return StringValue("");
- }
- uint8_t digest[SHA_DIGEST_LENGTH];
- SHA1(reinterpret_cast<const uint8_t*>(args[0]->data.c_str()), args[0]->data.size(), digest);
-
- if (argv.size() == 1) {
- return StringValue(print_sha1(digest));
+ auto source = Partition::Parse(args[1], &err);
+ if (!source) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse source \"%s\": %s", name,
+ args[1].c_str(), err.c_str());
}
- for (size_t i = 1; i < argv.size(); ++i) {
- uint8_t arg_digest[SHA_DIGEST_LENGTH];
- if (args[i]->type != VAL_STRING) {
- LOG(ERROR) << name << "(): arg " << i << " is not a string; skipping";
- } else if (ParseSha1(args[i]->data.c_str(), arg_digest) != 0) {
- // Warn about bad args and skip them.
- LOG(ERROR) << name << "(): error parsing \"" << args[i]->data << "\" as sha-1; skipping";
- } else if (memcmp(digest, arg_digest, SHA_DIGEST_LENGTH) == 0) {
- // Found a match.
- return args[i].release();
- }
+ std::vector<std::unique_ptr<Value>> values;
+ if (!ReadValueArgs(state, argv, &values, 2, 1) || values[0]->type != Value::Type::BLOB) {
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Invalid patch arg", name);
}
- // Didn't match any of the hex strings; return false.
- return StringValue("");
+ bool result = PatchPartition(target, source, *values[0], nullptr);
+ return StringValue(result ? "t" : "");
}
// mount(fs_type, partition_type, location, mount_point)
@@ -511,8 +453,8 @@
}
if (fs_type == "ext4") {
- const char* mke2fs_argv[] = { "/sbin/mke2fs_static", "-t", "ext4", "-b", "4096",
- location.c_str(), nullptr, nullptr };
+ const char* mke2fs_argv[] = { "/system/bin/mke2fs", "-t", "ext4", "-b", "4096",
+ location.c_str(), nullptr, nullptr };
std::string size_str;
if (size != 0) {
size_str = std::to_string(size / 4096LL);
@@ -525,8 +467,8 @@
return StringValue("");
}
- const char* e2fsdroid_argv[] = { "/sbin/e2fsdroid_static", "-e", "-a", mount_point.c_str(),
- location.c_str(), nullptr };
+ 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) {
LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location;
@@ -661,33 +603,12 @@
const std::string& filename = args[0];
const std::string& key = args[1];
- struct stat st;
- if (stat(filename.c_str(), &st) < 0) {
- return ErrorAbort(state, kFileGetPropFailure, "%s: failed to stat \"%s\": %s", name,
- filename.c_str(), strerror(errno));
- }
-
- constexpr off_t MAX_FILE_GETPROP_SIZE = 65536;
- if (st.st_size > MAX_FILE_GETPROP_SIZE) {
- return ErrorAbort(state, kFileGetPropFailure, "%s too large for %s (max %lld)",
- filename.c_str(), name, static_cast<long long>(MAX_FILE_GETPROP_SIZE));
- }
-
- std::string buffer(st.st_size, '\0');
- unique_file f(ota_fopen(filename.c_str(), "rb"));
- if (f == nullptr) {
- return ErrorAbort(state, kFileOpenFailure, "%s: failed to open %s: %s", name, filename.c_str(),
- strerror(errno));
- }
-
- if (ota_fread(&buffer[0], 1, st.st_size, f.get()) != static_cast<size_t>(st.st_size)) {
- ErrorAbort(state, kFreadFailure, "%s: failed to read %zu bytes from %s", name,
- static_cast<size_t>(st.st_size), filename.c_str());
+ std::string buffer;
+ if (!android::base::ReadFileToString(filename, &buffer)) {
+ ErrorAbort(state, kFreadFailure, "%s: failed to read %s", name, filename.c_str());
return nullptr;
}
- ota_fclose(f);
-
std::vector<std::string> lines = android::base::Split(buffer, "\n");
for (size_t i = 0; i < lines.size(); i++) {
std::string line = android::base::Trim(lines[i]);
@@ -733,7 +654,7 @@
}
// Skip the cache size check if the update is a retry.
- if (state->is_retry || CacheSizeCheck(bytes) == 0) {
+ if (state->is_retry || CheckAndFreeSpaceOnCache(bytes)) {
return StringValue("t");
}
return StringValue("");
@@ -786,8 +707,8 @@
return StringValue(std::to_string(status));
}
-// Read a local file and return its contents (the Value* returned
-// is actually a FileContents*).
+// read_file(filename)
+// Reads a local file 'filename' and returns its contents as a string Value.
Value* ReadFileFn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) {
if (argv.size() != 1) {
return ErrorAbort(state, kArgsParsingFailure, "%s() expects 1 arg, got %zu", name, argv.size());
@@ -795,18 +716,18 @@
std::vector<std::string> args;
if (!ReadArgs(state, argv, &args)) {
- return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name);
+ return ErrorAbort(state, kArgsParsingFailure, "%s(): Failed to parse the argument(s)", name);
}
const std::string& filename = args[0];
- Value* v = new Value(VAL_INVALID, "");
-
- FileContents fc;
- if (LoadFileContents(filename.c_str(), &fc) == 0) {
- v->type = VAL_BLOB;
- v->data = std::string(fc.data.begin(), fc.data.end());
+ std::string contents;
+ if (android::base::ReadFileToString(filename, &contents)) {
+ return new Value(Value::Type::STRING, std::move(contents));
}
- return v;
+
+ // Leave it to caller to handle the failure.
+ PLOG(ERROR) << name << ": Failed to read " << filename;
+ return StringValue("");
}
// write_value(value, filename)
@@ -872,11 +793,7 @@
return StringValue("");
}
- std::string reboot_cmd = "reboot," + property;
- if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
- reboot_cmd += ",quiescent";
- }
- android::base::SetProperty(ANDROID_RB_PROPERTY, reboot_cmd);
+ reboot("reboot," + property);
sleep(5);
return ErrorAbort(state, kRebootFailure, "%s() failed to reboot", name);
@@ -964,7 +881,12 @@
if (!android::base::ParseUint(len_str.c_str(), &len)) {
return nullptr;
}
- unique_fd fd(ota_open(filename.c_str(), O_WRONLY, 0644));
+ android::base::unique_fd fd(open(filename.c_str(), O_WRONLY));
+ if (fd == -1) {
+ PLOG(ERROR) << "Failed to open " << filename;
+ return StringValue("");
+ }
+
// The wipe_block_device function in ext4_utils returns 0 on success and 1
// for failure.
int status = wipe_block_device(fd, len);
@@ -1022,14 +944,13 @@
RegisterFunction("getprop", GetPropFn);
RegisterFunction("file_getprop", FileGetPropFn);
- RegisterFunction("apply_patch", ApplyPatchFn);
- RegisterFunction("apply_patch_check", ApplyPatchCheckFn);
RegisterFunction("apply_patch_space", ApplyPatchSpaceFn);
+ RegisterFunction("patch_partition", PatchPartitionFn);
+ RegisterFunction("patch_partition_check", PatchPartitionCheckFn);
RegisterFunction("wipe_block_device", WipeBlockDeviceFn);
RegisterFunction("read_file", ReadFileFn);
- RegisterFunction("sha1_check", Sha1CheckFn);
RegisterFunction("write_value", WriteValueFn);
RegisterFunction("wipe_cache", WipeCacheFn);
diff --git a/updater/updater.cpp b/updater/updater.cpp
index 1d6b172..e87c57a 100644
--- a/updater/updater.cpp
+++ b/updater/updater.cpp
@@ -17,9 +17,9 @@
#include "updater/updater.h"
#include <stdio.h>
-#include <unistd.h>
#include <stdlib.h>
#include <string.h>
+#include <unistd.h>
#include <string>
@@ -31,11 +31,9 @@
#include <ziparchive/zip_archive.h>
#include "edify/expr.h"
-#include "otafault/config.h"
-#include "otautil/DirUtil.h"
-#include "otautil/SysUtil.h"
-#include "otautil/cache_location.h"
+#include "otautil/dirutil.h"
#include "otautil/error_code.h"
+#include "otautil/sysutil.h"
#include "updater/blockimg.h"
#include "updater/install.h"
@@ -48,8 +46,6 @@
// (Note it's "updateR-script", not the older "update-script".)
static constexpr const char* SCRIPT_NAME = "META-INF/com/google/android/updater-script";
-extern bool have_eio_error;
-
struct selabel_handle *sehandle;
static void UpdaterLogger(android::base::LogId /* id */, android::base::LogSeverity /* severity */,
@@ -135,7 +131,7 @@
std::unique_ptr<Expr> root;
int error_count = 0;
- int error = parse_string(script.c_str(), &root, &error_count);
+ int error = ParseString(script, &root, &error_count);
if (error != 0 || error_count > 0) {
LOG(ERROR) << error_count << " parse errors";
CloseArchive(za);
@@ -167,15 +163,10 @@
printf("unexpected argument: %s", argv[4]);
}
}
- ota_io_init(za, state.is_retry);
std::string result;
bool status = Evaluate(&state, root, &result);
- if (have_eio_error) {
- fprintf(cmd_pipe, "retry_update\n");
- }
-
if (!status) {
if (state.errmsg.empty()) {
LOG(ERROR) << "script aborted (no error message)";
@@ -207,6 +198,9 @@
if (state.cause_code == kPatchApplicationFailure) {
LOG(INFO) << "Patch application failed, retry update.";
fprintf(cmd_pipe, "retry_update\n");
+ } else if (state.cause_code == kEioFailure) {
+ LOG(INFO) << "Update failed due to EIO, retry update.";
+ fprintf(cmd_pipe, "retry_update\n");
}
}
diff --git a/updater_sample/.gitignore b/updater_sample/.gitignore
new file mode 100644
index 0000000..f846472
--- /dev/null
+++ b/updater_sample/.gitignore
@@ -0,0 +1,10 @@
+*~
+*.bak
+*.pyc
+*.pyc-2.4
+Thumbs.db
+*.iml
+.idea/
+gen/
+.vscode
+local.properties
diff --git a/updater_sample/Android.bp b/updater_sample/Android.bp
new file mode 100644
index 0000000..845e07b
--- /dev/null
+++ b/updater_sample/Android.bp
@@ -0,0 +1,33 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_app {
+ name: "SystemUpdaterSample",
+ sdk_version: "system_current",
+ privileged: true,
+
+ srcs: ["src/**/*.java"],
+
+ static_libs: [
+ "guava",
+ ],
+
+ optimize: {
+ proguard_flags_files: [
+ "proguard.flags",
+ ],
+ },
+
+ resource_dirs: ["res"],
+}
diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml
new file mode 100644
index 0000000..18d8425
--- /dev/null
+++ b/updater_sample/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 The Android Open Source Project
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.systemupdatersample">
+
+ <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
+
+ <uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
+
+ <application
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:roundIcon="@mipmap/ic_launcher_round">
+ <activity
+ android:name=".ui.MainActivity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ <service android:name=".services.PrepareStreamingService"/>
+ </application>
+
+</manifest>
diff --git a/updater_sample/OWNERS b/updater_sample/OWNERS
new file mode 100644
index 0000000..5c1c370
--- /dev/null
+++ b/updater_sample/OWNERS
@@ -0,0 +1,2 @@
+zhaojiac@google.com
+zhomart@google.com
diff --git a/updater_sample/README.md b/updater_sample/README.md
new file mode 100644
index 0000000..f9c3fb8
--- /dev/null
+++ b/updater_sample/README.md
@@ -0,0 +1,267 @@
+# SystemUpdaterSample
+
+This app demonstrates how to use Android system updates APIs to install
+[OTA updates](https://source.android.com/devices/tech/ota/). It contains a
+sample client for `update_engine` to install A/B (seamless) updates.
+
+A/B (seamless) update is available since Android Nougat (API 24), but this sample
+targets the latest android.
+
+
+## Workflow
+
+SystemUpdaterSample app shows list of available updates on the UI. User is allowed
+to select an update and apply it to the device. App shows installation progress,
+logs can be found in `adb logcat`. User can stop or reset an update. Resetting
+the update requests update engine to cancel any ongoing update, and revert
+if the update has been applied. Stopping does not revert the applied update.
+
+
+## Update Config file
+
+In this sample updates are defined in JSON update config files.
+The structure of a config file is defined in
+`com.example.android.systemupdatersample.UpdateConfig`, example file is located
+at `res/raw/sample.json`.
+
+In real-life update system the config files expected to be served from a server
+to the app, but in this sample, the config files are stored on the device.
+The directory can be found in logs or on the UI. In most cases it should be located at
+`/data/user/0/com.example.android.systemupdatersample/files/configs/`.
+
+SystemUpdaterSample app downloads OTA package from `url`. In this sample app
+`url` is expected to point to file system, e.g. `file:///data/my-sample-ota-builds-dir/ota-002.zip`.
+
+If `ab_install_type` is `NON_STREAMING` then app checks if `url` starts
+with `file://` and passes `url` to the `update_engine`.
+
+If `ab_install_type` is `STREAMING`, app downloads only the entries in need, as
+opposed to the entire package, to initiate a streaming update. The `payload.bin`
+entry, which takes up the majority of the space in an OTA package, will be
+streamed by `update_engine` directly. The ZIP entries in such a package need to be
+saved uncompressed (`ZIP_STORED`), so that their data can be downloaded directly
+with the offset and length. As `payload.bin` itself is already in compressed
+format, the size penalty is marginal.
+
+if `ab_config.force_switch_slot` set true device will boot to the
+updated partition on next reboot; otherwise button "Switch Slot" will
+become active, and user can manually set updated partition as the active slot.
+
+Config files can be generated using `tools/gen_update_config.py`.
+Running `./tools/gen_update_config.py --help` shows usage of the script.
+
+
+## Sample App State vs UpdateEngine Status
+
+UpdateEngine provides status for different stages of update application
+process. But it lacks of proper status codes when update fails.
+
+This creates two problems:
+
+1. If sample app is unbound from update_engine (MainActivity is paused, destroyed),
+ app doesn't receive onStatusUpdate and onPayloadApplicationCompleted notifications.
+ If app binds to update_engine after update is completed,
+ only onStatusUpdate is called, but status becomes IDLE in most cases.
+ And there is no way to know if update was successful or not.
+
+2. This sample app demostrates suspend/resume using update_engins's
+ `cancel` and `applyPayload` (which picks up from where it left).
+ When `cancel` is called, status is set to `IDLE`, which doesn't allow
+ tracking suspended state properly.
+
+To solve these problems sample app implements its own separate update
+state - `UpdaterState`. To solve the first problem, sample app persists
+`UpdaterState` on a device. When app is resumed, it checks if `UpdaterState`
+matches the update_engine's status (as onStatusUpdate is guaranteed to be called).
+If they doesn't match, sample app calls `applyPayload` again with the same
+parameters, and handles update completion properly using `onPayloadApplicationCompleted`
+callback. The second problem is solved by adding `PAUSED` updater state.
+
+
+## Sample App UI
+
+### Text fields
+
+- `Current Build:` - shows current active build.
+- `Updater state:` - SystemUpdaterSample app state.
+- `Engine status:` - last reported update_engine status.
+- `Engine error:` - last reported payload application error.
+
+### Buttons
+
+- `Reload` - reloads update configs from device storage.
+- `View config` - shows selected update config.
+- `Apply` - applies selected update config.
+- `Stop` - cancel running update, calls `UpdateEngine#cancel`.
+- `Reset` - reset update, calls `UpdateEngine#resetStatus`, can be called
+ only when update is not running.
+- `Suspend` - suspend running update, uses `UpdateEngine#cancel`.
+- `Resume` - resumes suspended update, uses `UpdateEngine#applyPayload`.
+- `Switch Slot` - if `ab_config.force_switch_slot` config set true,
+ this button will be enabled after payload is applied,
+ to switch A/B slot on next reboot.
+
+
+## Sending HTTP headers from UpdateEngine
+
+Sometimes OTA package server might require some HTTP headers to be present,
+e.g. `Authorization` header to contain valid auth token. While performing
+streaming update, `UpdateEngine` allows passing on certain HTTP headers;
+as of writing this sample app, these headers are `Authorization` and `User-Agent`.
+
+`android.os.UpdateEngine#applyPayload` contains information on
+which HTTP headers are supported.
+
+
+## Used update_engine APIs
+
+### UpdateEngine#bind
+
+Binds given callbacks to update_engine. When update_engine successfully
+initialized, it's guaranteed to invoke callback onStatusUpdate.
+
+### UpdateEngine#applyPayload
+
+Start an update attempt to download an apply the provided `payload_url` if
+no other update is running. The extra `key_value_pair_headers` will be
+included when fetching the payload.
+
+`key_value_pair_headers` argument also accepts properties other than HTTP Headers.
+List of allowed properties can be found in `system/update_engine/common/constants.cc`.
+
+### UpdateEngine#cancel
+
+Cancel the ongoing update. The update could be running or suspended, but it
+can't be canceled after it was done.
+
+### UpdateEngine#resetStatus
+
+Reset the already applied update back to an idle state. This method can
+only be called when no update attempt is going on, and it will reset the
+status back to idle, deleting the currently applied update if any.
+
+### Callback: onStatusUpdate
+
+Called whenever the value of `status` or `progress` changes. For
+`progress` values changes, this method will be called only if it changes significantly.
+At this time of writing this doc, delta for `progress` is `0.005`.
+
+`onStatusUpdate` is always called when app binds to update_engine,
+except when update_engine fails to initialize.
+
+### Callback: onPayloadApplicationComplete
+
+Called whenever an update attempt is completed or failed.
+
+
+## Running on a device
+
+The commands are expected to be run from `$ANDROID_BUILD_TOP` and for demo
+purpose only.
+
+### Without the privileged system permissions
+
+1. Compile the app `mmma -j bootable/recovery/updater_sample`.
+2. Install the app to the device using `adb install <APK_PATH>`.
+3. Change permissions on `/data/ota_package/` to `0777` on the device.
+4. Set SELinux mode to permissive. See instructions below.
+5. Add update config files; look above at [Update Config file](#Update-Config-file).
+6. Push OTA packages to the device.
+7. Run the sample app.
+
+### With the privileged system permissions
+
+To run sample app as a privileged system app, it needs to be installed in `/system/priv-app/`.
+This directory is expected to be read-only, unless explicitly remounted.
+
+The recommended way to run the app is to build and install it as a
+privileged system app, so it's granted the required permissions to access
+`update_engine` service as well as OTA package files. Detailed steps are as follows:
+
+1. [Prepare to build](https://source.android.com/setup/build/building)
+2. Add the module (SystemUpdaterSample) to the `PRODUCT_PACKAGES` list for the
+ lunch target.
+ e.g. add a line containing `PRODUCT_PACKAGES += SystemUpdaterSample`
+ to `device/google/marlin/device-common.mk`.
+3. [Whitelist the sample app](https://source.android.com/devices/tech/config/perms-whitelist)
+ * Add
+ ```
+ <privapp-permissions package="com.example.android.systemupdatersample">
+ <permission name="android.permission.ACCESS_CACHE_FILESYSTEM"/>
+ </privapp-permissions>
+ ```
+ to `frameworks/base/data/etc/privapp-permissions-platform.xml`
+5. Build sample app `mmma -j bootable/recovery/updater_sample`.
+6. Build Android `make -j`
+7. [Flash the device](https://source.android.com/setup/build/running)
+8. Add update config files; look above at `## Update Config file`;
+ `adb root` might be required.
+9. Push OTA packages to the device if there is no server to stream packages from;
+ changing of SELinux labels of OTA packages directory might be required
+ `chcon -R u:object_r:ota_package_file:s0 /data/my-sample-ota-builds-dir`
+10. Run the sample app.
+
+
+## Development
+
+- [x] Create a UI with list of configs, current version,
+ control buttons, progress bar and log viewer
+- [x] Add `PayloadSpec` and `PayloadSpecs` for working with
+ update zip file
+- [x] Add `UpdateConfig` for working with json config files
+- [x] Add applying non-streaming update
+- [x] Prepare streaming update (partially downloading package)
+- [x] Add applying streaming update
+- [x] Add stop/reset the update
+- [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload`
+- [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules)
+- [x] Deferred switch slot demo
+- [x] Add UpdateManager; extract update logic from MainActivity
+- [x] Add Sample app update state (separate from update_engine status)
+- [x] Add smart update completion detection using onStatusUpdate
+- [x] Add pause/resume demo
+- [x] Verify system partition checksum for package
+
+
+## Running tests
+
+1. Build `mmma bootable/recovery/updater_sample/`
+2. Install app
+ `adb install $OUT/system/priv-app/SystemUpdaterSample/SystemUpdaterSample.apk`
+3. Install tests
+ `adb install $OUT/testcases/SystemUpdaterSampleTests/SystemUpdaterSampleTests.apk`
+4. Run tests
+ `adb shell am instrument -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner`
+5. Run a test file
+ ```
+ adb shell am instrument \
+ -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \
+ -c com.example.android.systemupdatersample.util.PayloadSpecsTest
+ ```
+
+
+## Accessing `android.os.UpdateEngine` API
+
+`android.os.UpdateEngine` APIs are marked as `@SystemApi`, meaning only system
+apps can access them.
+
+
+## Getting read/write access to `/data/ota_package/`
+
+Access to cache filesystem is granted only to system apps.
+
+
+## Setting SELinux mode to permissive (0)
+
+```txt
+local$ adb root
+local$ adb shell
+android# setenforce 0
+android# getenforce
+```
+
+
+## License
+
+SystemUpdaterSample app is released under
+[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
diff --git a/tools/dumpkey/Android.mk b/updater_sample/proguard.flags
similarity index 60%
rename from tools/dumpkey/Android.mk
rename to updater_sample/proguard.flags
index 3154914..97ab534 100644
--- a/tools/dumpkey/Android.mk
+++ b/updater_sample/proguard.flags
@@ -1,4 +1,4 @@
-# Copyright (C) 2008 The Android Open Source Project
+# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -12,11 +12,13 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-LOCAL_PATH := $(call my-dir)
+# Keep, used in tests.
+-keep public class com.example.android.systemupdatersample.UpdateManager {
+ public int getUpdaterState();
+}
-include $(CLEAR_VARS)
-LOCAL_MODULE := dumpkey
-LOCAL_SRC_FILES := DumpPublicKey.java
-LOCAL_JAR_MANIFEST := DumpPublicKey.mf
-LOCAL_STATIC_JAVA_LIBRARIES := bouncycastle-host
-include $(BUILD_HOST_JAVA_LIBRARY)
+# Keep, used in tests.
+-keep public class com.example.android.systemupdatersample.UpdateConfig {
+ public <init>(java.lang.String, java.lang.String, int);
+}
+
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
new file mode 100644
index 0000000..b560827
--- /dev/null
+++ b/updater_sample/res/layout/activity_main.xml
@@ -0,0 +1,244 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:padding="4dip"
+ android:gravity="center_horizontal"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_marginBottom="8dp"
+ android:layout_marginEnd="8dp"
+ android:layout_marginStart="8dp"
+ android:layout_marginTop="8dp"
+ >
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:orientation="vertical">
+
+ <TextView
+ android:id="@+id/textViewBuildtitle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Current Build:" />
+
+ <TextView
+ android:id="@+id/textViewBuild"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/unknown" />
+
+ <Space
+ android:layout_width="match_parent"
+ android:layout_height="40dp" />
+
+ <TextView
+ android:id="@+id/textView4"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="Apply an update" />
+
+ <TextView
+ android:id="@+id/textViewConfigsDirHint"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:text="Config files located in NULL"
+ android:textColor="#777"
+ android:textSize="10sp"
+ android:textStyle="italic" />
+
+ <Spinner
+ android:id="@+id/spinnerConfigs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="8dp"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/buttonReload"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onReloadClick"
+ android:text="Reload" />
+
+ <Button
+ android:id="@+id/buttonViewConfig"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onViewConfigClick"
+ android:text="View config" />
+
+ <Button
+ android:id="@+id/buttonApplyConfig"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onApplyConfigClick"
+ android:text="Apply" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="24dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Updater state:" />
+
+ <TextView
+ android:id="@+id/textViewUpdaterState"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:text="@string/unknown" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Engine status:" />
+
+ <TextView
+ android:id="@+id/textViewEngineStatus"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:text="@string/unknown" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="4dp"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/textView2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Engine error:" />
+
+ <TextView
+ android:id="@+id/textViewEngineErrorCode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginLeft="8dp"
+ android:text="@string/unknown" />
+ </LinearLayout>
+
+
+ <ProgressBar
+ android:id="@+id/progressBar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_marginTop="8dp"
+ android:min="0"
+ android:max="100"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/buttonStop"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onStopClick"
+ android:text="Stop" />
+
+ <Button
+ android:id="@+id/buttonReset"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onResetClick"
+ android:text="Reset" />
+ </LinearLayout>
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="12dp"
+ android:orientation="horizontal">
+
+ <Button
+ android:id="@+id/buttonSuspend"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onSuspendClick"
+ android:text="Suspend" />
+
+ <Button
+ android:id="@+id/buttonResume"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:onClick="onResumeClick"
+ android:text="Resume" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/textViewUpdateInfo"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="14dp"
+ android:textColor="#777"
+ android:textSize="10sp"
+ android:textStyle="italic"
+ android:text="@string/finish_update_info" />
+
+ <Button
+ android:id="@+id/buttonSwitchSlot"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:onClick="onSwitchSlotClick"
+ android:text="@string/switch_slot" />
+
+ </LinearLayout>
+
+ </ScrollView>
+
+</LinearLayout>
diff --git a/updater_sample/res/mipmap-hdpi/ic_launcher.png b/updater_sample/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..a2f5908
--- /dev/null
+++ b/updater_sample/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/updater_sample/res/mipmap-hdpi/ic_launcher_round.png b/updater_sample/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..1b52399
--- /dev/null
+++ b/updater_sample/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json
new file mode 100644
index 0000000..f188c23
--- /dev/null
+++ b/updater_sample/res/raw/sample.json
@@ -0,0 +1,29 @@
+{
+ "__name": "name will be visible on UI",
+ "__url": "https:// or file:// uri to update package (zip, xz, ...)",
+ "__ab_install_type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
+ "name": "SAMPLE-cake-release BUILD-12345",
+ "url": "http://foo.bar/builds/ota-001.zip",
+ "ab_install_type": "NON_STREAMING",
+ "ab_streaming_metadata": {
+ "__": "streaming_metadata is required only for streaming update",
+ "__property_files": "name, offset and size of files",
+ "__authorization": "it will be sent to OTA package server as value of HTTP header - Authorization",
+ "property_files": [
+ {
+ "__filename": "name of the file in package",
+ "__offset": "defines beginning of the file in package",
+ "__size": "size of the file in package",
+ "filename": "payload.bin",
+ "offset": 531,
+ "size": 5012323
+ }
+ ],
+ "authorization": "Basic my-secret-token"
+ },
+ "ab_config": {
+ "__": "A/B (seamless) update configurations",
+ "__force_switch_slot": "if set true device will boot to a new slot, otherwise user manually switches slot on the screen",
+ "force_switch_slot": false
+ }
+}
diff --git a/updater_sample/res/values/strings.xml b/updater_sample/res/values/strings.xml
new file mode 100644
index 0000000..db4a5dc
--- /dev/null
+++ b/updater_sample/res/values/strings.xml
@@ -0,0 +1,23 @@
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <string name="app_name">SystemUpdaterSample</string>
+ <string name="action_reload">Reload</string>
+ <string name="unknown">Unknown</string>
+ <string name="close">CLOSE</string>
+ <string name="switch_slot">Switch Slot</string>
+ <string name="finish_update_info">To finish the update press the button below</string>
+</resources>
diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
new file mode 100644
index 0000000..ce88338
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.UpdateEngine;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * Payload that will be given to {@link UpdateEngine#applyPayload)}.
+ */
+public class PayloadSpec implements Serializable {
+
+ private static final long serialVersionUID = 41043L;
+
+ /**
+ * Creates a payload spec {@link Builder}
+ */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ private String mUrl;
+ private long mOffset;
+ private long mSize;
+ private List<String> mProperties;
+
+ public PayloadSpec(Builder b) {
+ this.mUrl = b.mUrl;
+ this.mOffset = b.mOffset;
+ this.mSize = b.mSize;
+ this.mProperties = b.mProperties;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public long getOffset() {
+ return mOffset;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+
+ public List<String> getProperties() {
+ return mProperties;
+ }
+
+ /**
+ * payload spec builder.
+ *
+ * <p>Usage:</p>
+ *
+ * {@code
+ * PayloadSpec spec = PayloadSpec.newBuilder()
+ * .url("url")
+ * .build();
+ * }
+ */
+ public static class Builder {
+ private String mUrl;
+ private long mOffset;
+ private long mSize;
+ private List<String> mProperties;
+
+ public Builder() {
+ }
+
+ /**
+ * set url
+ */
+ public Builder url(String url) {
+ this.mUrl = url;
+ return this;
+ }
+
+ /**
+ * set offset
+ */
+ public Builder offset(long offset) {
+ this.mOffset = offset;
+ return this;
+ }
+
+ /**
+ * set size
+ */
+ public Builder size(long size) {
+ this.mSize = size;
+ return this;
+ }
+
+ /**
+ * set properties
+ */
+ public Builder properties(List<String> properties) {
+ this.mProperties = properties;
+ return this;
+ }
+
+ /**
+ * build {@link PayloadSpec}
+ */
+ public PayloadSpec build() {
+ return new PayloadSpec(this);
+ }
+ }
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
new file mode 100644
index 0000000..61872a6
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Optional;
+
+/**
+ * An update description. It will be parsed from JSON, which is intended to
+ * be sent from server to the update app, but in this sample app it will be stored on the device.
+ */
+public class UpdateConfig implements Parcelable {
+
+ public static final int AB_INSTALL_TYPE_NON_STREAMING = 0;
+ public static final int AB_INSTALL_TYPE_STREAMING = 1;
+
+ public static final Parcelable.Creator<UpdateConfig> CREATOR =
+ new Parcelable.Creator<UpdateConfig>() {
+ @Override
+ public UpdateConfig createFromParcel(Parcel source) {
+ return new UpdateConfig(source);
+ }
+
+ @Override
+ public UpdateConfig[] newArray(int size) {
+ return new UpdateConfig[size];
+ }
+ };
+
+ /** parse update config from json */
+ public static UpdateConfig fromJson(String json) throws JSONException {
+ UpdateConfig c = new UpdateConfig();
+
+ JSONObject o = new JSONObject(json);
+ c.mName = o.getString("name");
+ c.mUrl = o.getString("url");
+ switch (o.getString("ab_install_type")) {
+ case AB_INSTALL_TYPE_NON_STREAMING_JSON:
+ c.mAbInstallType = AB_INSTALL_TYPE_NON_STREAMING;
+ break;
+ case AB_INSTALL_TYPE_STREAMING_JSON:
+ c.mAbInstallType = AB_INSTALL_TYPE_STREAMING;
+ break;
+ default:
+ throw new JSONException("Invalid type, expected either "
+ + "NON_STREAMING or STREAMING, got " + o.getString("ab_install_type"));
+ }
+
+ // TODO: parse only for A/B updates when non-A/B is implemented
+ JSONObject ab = o.getJSONObject("ab_config");
+ boolean forceSwitchSlot = ab.getBoolean("force_switch_slot");
+ boolean verifyPayloadMetadata = ab.getBoolean("verify_payload_metadata");
+ ArrayList<PackageFile> propertyFiles = new ArrayList<>();
+ if (ab.has("property_files")) {
+ JSONArray propertyFilesJson = ab.getJSONArray("property_files");
+ for (int i = 0; i < propertyFilesJson.length(); i++) {
+ JSONObject p = propertyFilesJson.getJSONObject(i);
+ propertyFiles.add(new PackageFile(
+ p.getString("filename"),
+ p.getLong("offset"),
+ p.getLong("size")));
+ }
+ }
+ String authorization = ab.optString("authorization", null);
+ c.mAbConfig = new AbConfig(
+ forceSwitchSlot,
+ verifyPayloadMetadata,
+ propertyFiles.toArray(new PackageFile[0]),
+ authorization);
+
+ c.mRawJson = json;
+ return c;
+ }
+
+ /**
+ * these strings are represent types in JSON config files
+ */
+ private static final String AB_INSTALL_TYPE_NON_STREAMING_JSON = "NON_STREAMING";
+ private static final String AB_INSTALL_TYPE_STREAMING_JSON = "STREAMING";
+
+ /** name will be visible on UI */
+ private String mName;
+
+ /** update zip file URI, can be https:// or file:// */
+ private String mUrl;
+
+ /** non-streaming (first saves locally) OR streaming (on the fly) */
+ private int mAbInstallType;
+
+ /** A/B update configurations */
+ private AbConfig mAbConfig;
+
+ private String mRawJson;
+
+ protected UpdateConfig() {
+ }
+
+ protected UpdateConfig(Parcel in) {
+ this.mName = in.readString();
+ this.mUrl = in.readString();
+ this.mAbInstallType = in.readInt();
+ this.mAbConfig = (AbConfig) in.readSerializable();
+ this.mRawJson = in.readString();
+ }
+
+ public UpdateConfig(String name, String url, int installType) {
+ this.mName = name;
+ this.mUrl = url;
+ this.mAbInstallType = installType;
+ }
+
+ public String getName() {
+ return mName;
+ }
+
+ public String getUrl() {
+ return mUrl;
+ }
+
+ public String getRawJson() {
+ return mRawJson;
+ }
+
+ public int getInstallType() {
+ return mAbInstallType;
+ }
+
+ public AbConfig getAbConfig() {
+ return mAbConfig;
+ }
+
+ /**
+ * @return File object for given url
+ */
+ public File getUpdatePackageFile() {
+ if (mAbInstallType != AB_INSTALL_TYPE_NON_STREAMING) {
+ throw new RuntimeException("Expected non-streaming install type");
+ }
+ if (!mUrl.startsWith("file://")) {
+ throw new RuntimeException("url is expected to start with file://");
+ }
+ return new File(mUrl.substring(7, mUrl.length()));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mName);
+ dest.writeString(mUrl);
+ dest.writeInt(mAbInstallType);
+ dest.writeSerializable(mAbConfig);
+ dest.writeString(mRawJson);
+ }
+
+ /**
+ * Description of a file in an OTA package zip file.
+ */
+ public static class PackageFile implements Serializable {
+
+ private static final long serialVersionUID = 31043L;
+
+ /** filename in an archive */
+ private String mFilename;
+
+ /** defines beginning of update data in archive */
+ private long mOffset;
+
+ /** size of the update data in archive */
+ private long mSize;
+
+ public PackageFile(String filename, long offset, long size) {
+ this.mFilename = filename;
+ this.mOffset = offset;
+ this.mSize = size;
+ }
+
+ public String getFilename() {
+ return mFilename;
+ }
+
+ public long getOffset() {
+ return mOffset;
+ }
+
+ public long getSize() {
+ return mSize;
+ }
+ }
+
+ /**
+ * A/B (seamless) update configurations.
+ */
+ public static class AbConfig implements Serializable {
+
+ private static final long serialVersionUID = 31044L;
+
+ /**
+ * if set true device will boot to new slot, otherwise user manually
+ * switches slot on the screen.
+ */
+ private boolean mForceSwitchSlot;
+
+ /**
+ * if set true device will boot to new slot, otherwise user manually
+ * switches slot on the screen.
+ */
+ private boolean mVerifyPayloadMetadata;
+
+ /** defines beginning of update data in archive */
+ private PackageFile[] mPropertyFiles;
+
+ /**
+ * SystemUpdaterSample receives the authorization token from the OTA server, in addition
+ * to the package URL. It passes on the info to update_engine, so that the latter can
+ * fetch the data from the package server directly with the token.
+ */
+ private String mAuthorization;
+
+ public AbConfig(
+ boolean forceSwitchSlot,
+ boolean verifyPayloadMetadata,
+ PackageFile[] propertyFiles,
+ String authorization) {
+ this.mForceSwitchSlot = forceSwitchSlot;
+ this.mVerifyPayloadMetadata = verifyPayloadMetadata;
+ this.mPropertyFiles = propertyFiles;
+ this.mAuthorization = authorization;
+ }
+
+ public boolean getForceSwitchSlot() {
+ return mForceSwitchSlot;
+ }
+
+ public boolean getVerifyPayloadMetadata() {
+ return mVerifyPayloadMetadata;
+ }
+
+ public PackageFile[] getPropertyFiles() {
+ return mPropertyFiles;
+ }
+
+ public Optional<String> getAuthorization() {
+ return mAuthorization == null ? Optional.empty() : Optional.of(mAuthorization);
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
new file mode 100644
index 0000000..12a8f3f
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.content.Context;
+import android.os.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.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;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.DoubleConsumer;
+import java.util.function.IntConsumer;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Manages the update flow. It has its own state (in memory), separate from
+ * {@link UpdateEngine}'s state. Asynchronously interacts with the {@link UpdateEngine}.
+ */
+public class UpdateManager {
+
+ private static final String TAG = "UpdateManager";
+
+ /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */
+ private 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);
+ private AtomicInteger mEngineErrorCode = new AtomicInteger(UpdateEngineErrorCodes.UNKNOWN);
+ private AtomicDouble mProgress = new AtomicDouble(0);
+ private UpdaterState mUpdaterState = new UpdaterState(UpdaterState.IDLE);
+
+ private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
+
+ /** Synchronize state with engine status only once when app binds to UpdateEngine. */
+ private AtomicBoolean mStateSynchronized = new AtomicBoolean(false);
+
+ @GuardedBy("mLock")
+ private UpdateData mLastUpdateData = null;
+
+ @GuardedBy("mLock")
+ private IntConsumer mOnStateChangeCallback = null;
+ @GuardedBy("mLock")
+ private IntConsumer mOnEngineStatusUpdateCallback = null;
+ @GuardedBy("mLock")
+ private DoubleConsumer mOnProgressUpdateCallback = null;
+ @GuardedBy("mLock")
+ private IntConsumer mOnEngineCompleteCallback = null;
+
+ private final Object mLock = new Object();
+
+ private final UpdateManager.UpdateEngineCallbackImpl
+ mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl();
+
+ public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) {
+ this.mUpdateEngine = updateEngine;
+ this.mPayloadSpecs = payloadSpecs;
+ }
+
+ /**
+ * Binds to {@link UpdateEngine}. Invokes onStateChangeCallback if present.
+ */
+ public void bind() {
+ getOnStateChangeCallback().ifPresent(callback -> callback.accept(mUpdaterState.get()));
+
+ mStateSynchronized.set(false);
+ this.mUpdateEngine.bind(mUpdateEngineCallback);
+ }
+
+ /**
+ * Unbinds from {@link UpdateEngine}.
+ */
+ public void unbind() {
+ this.mUpdateEngine.unbind();
+ }
+
+ public int getUpdaterState() {
+ return mUpdaterState.get();
+ }
+
+ /**
+ * Returns true if manual switching slot is required. Value depends on
+ * the update config {@code ab_config.force_switch_slot}.
+ */
+ public boolean isManualSwitchSlotRequired() {
+ return mManualSwitchSlotRequired.get();
+ }
+
+ /**
+ * Sets SystemUpdaterSample app state change callback. Value of {@code state} will be one
+ * of the values from {@link UpdaterState}.
+ *
+ * @param onStateChangeCallback a callback with parameter {@code state}.
+ */
+ public void setOnStateChangeCallback(IntConsumer onStateChangeCallback) {
+ synchronized (mLock) {
+ this.mOnStateChangeCallback = onStateChangeCallback;
+ }
+ }
+
+ private Optional<IntConsumer> getOnStateChangeCallback() {
+ synchronized (mLock) {
+ return mOnStateChangeCallback == null
+ ? Optional.empty()
+ : Optional.of(mOnStateChangeCallback);
+ }
+ }
+
+ /**
+ * Sets update engine status update callback. Value of {@code status} will
+ * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
+ *
+ * @param onStatusUpdateCallback a callback with parameter {@code status}.
+ */
+ public void setOnEngineStatusUpdateCallback(IntConsumer onStatusUpdateCallback) {
+ synchronized (mLock) {
+ this.mOnEngineStatusUpdateCallback = onStatusUpdateCallback;
+ }
+ }
+
+ private Optional<IntConsumer> getOnEngineStatusUpdateCallback() {
+ synchronized (mLock) {
+ return mOnEngineStatusUpdateCallback == null
+ ? Optional.empty()
+ : Optional.of(mOnEngineStatusUpdateCallback);
+ }
+ }
+
+ /**
+ * Sets update engine payload application complete callback. Value of {@code errorCode} will
+ * be one of the values from {@link UpdateEngine.ErrorCodeConstants}.
+ *
+ * @param onComplete a callback with parameter {@code errorCode}.
+ */
+ public void setOnEngineCompleteCallback(IntConsumer onComplete) {
+ synchronized (mLock) {
+ this.mOnEngineCompleteCallback = onComplete;
+ }
+ }
+
+ private Optional<IntConsumer> getOnEngineCompleteCallback() {
+ synchronized (mLock) {
+ return mOnEngineCompleteCallback == null
+ ? Optional.empty()
+ : Optional.of(mOnEngineCompleteCallback);
+ }
+ }
+
+ /**
+ * Sets progress update callback. Progress is a number from {@code 0.0} to {@code 1.0}.
+ *
+ * @param onProgressCallback a callback with parameter {@code progress}.
+ */
+ public void setOnProgressUpdateCallback(DoubleConsumer onProgressCallback) {
+ synchronized (mLock) {
+ this.mOnProgressUpdateCallback = onProgressCallback;
+ }
+ }
+
+ private Optional<DoubleConsumer> getOnProgressUpdateCallback() {
+ synchronized (mLock) {
+ return mOnProgressUpdateCallback == null
+ ? Optional.empty()
+ : Optional.of(mOnProgressUpdateCallback);
+ }
+ }
+
+ /**
+ * Suspend running update.
+ */
+ public synchronized void suspend() throws UpdaterState.InvalidTransitionException {
+ Log.d(TAG, "suspend invoked");
+ setUpdaterState(UpdaterState.PAUSED);
+ mUpdateEngine.cancel();
+ }
+
+ /**
+ * Resume suspended update.
+ */
+ public synchronized void resume() throws UpdaterState.InvalidTransitionException {
+ Log.d(TAG, "resume invoked");
+ setUpdaterState(UpdaterState.RUNNING);
+ updateEngineReApplyPayload();
+ }
+
+ /**
+ * Updates {@link this.mState} and if state is changed,
+ * it also notifies {@link this.mOnStateChangeCallback}.
+ */
+ private void setUpdaterState(int newUpdaterState)
+ throws UpdaterState.InvalidTransitionException {
+ Log.d(TAG, "setUpdaterState invoked newState=" + newUpdaterState);
+ int previousState = mUpdaterState.get();
+ mUpdaterState.set(newUpdaterState);
+ if (previousState != newUpdaterState) {
+ getOnStateChangeCallback().ifPresent(callback -> callback.accept(newUpdaterState));
+ }
+ }
+
+ /**
+ * Same as {@link this.setUpdaterState}. Logs the error if new state
+ * cannot be set.
+ */
+ private void setUpdaterStateSilent(int newUpdaterState) {
+ try {
+ setUpdaterState(newUpdaterState);
+ } catch (UpdaterState.InvalidTransitionException e) {
+ // Most likely UpdateEngine status and UpdaterSample state got de-synchronized.
+ // To make sample app simple, we don't handle it properly.
+ Log.e(TAG, "Failed to set updater state", e);
+ }
+ }
+
+ /**
+ * Creates new UpdaterState, assigns it to {@link this.mUpdaterState},
+ * and notifies callbacks.
+ */
+ private void initializeUpdateState(int state) {
+ this.mUpdaterState = new UpdaterState(state);
+ getOnStateChangeCallback().ifPresent(callback -> callback.accept(state));
+ }
+
+ /**
+ * Requests update engine to stop any ongoing update. If an update has been applied,
+ * leave it as is.
+ */
+ public synchronized void cancelRunningUpdate() throws UpdaterState.InvalidTransitionException {
+ Log.d(TAG, "cancelRunningUpdate invoked");
+ setUpdaterState(UpdaterState.IDLE);
+ mUpdateEngine.cancel();
+ }
+
+ /**
+ * Resets update engine to IDLE state. If an update has been applied it reverts it.
+ */
+ public synchronized void resetUpdate() throws UpdaterState.InvalidTransitionException {
+ Log.d(TAG, "resetUpdate invoked");
+ setUpdaterState(UpdaterState.IDLE);
+ mUpdateEngine.resetStatus();
+ }
+
+ /**
+ * Applies the given update.
+ *
+ * <p>UpdateEngine works asynchronously. This method doesn't wait until
+ * end of the update.</p>
+ */
+ public synchronized void applyUpdate(Context context, UpdateConfig config)
+ throws UpdaterState.InvalidTransitionException {
+ mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
+ setUpdaterState(UpdaterState.RUNNING);
+
+ synchronized (mLock) {
+ // Cleaning up previous update data.
+ mLastUpdateData = null;
+ }
+
+ if (!config.getAbConfig().getForceSwitchSlot()) {
+ mManualSwitchSlotRequired.set(true);
+ } else {
+ mManualSwitchSlotRequired.set(false);
+ }
+
+ 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);
+ setUpdaterStateSilent(UpdaterState.ERROR);
+ }
+ });
+ }
+
+ private List<String> prepareExtraProperties(UpdateConfig config) {
+ List<String> extraProperties = new ArrayList<>();
+
+ if (!config.getAbConfig().getForceSwitchSlot()) {
+ // Disable switch slot on reboot, which is enabled by default.
+ // User will enable it manually by clicking "Switch Slot" button on the screen.
+ extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT);
+ }
+ return extraProperties;
+ }
+
+ /**
+ * Applies given payload.
+ *
+ * <p>UpdateEngine works asynchronously. This method doesn't wait until
+ * end of the update.</p>
+ *
+ * <p>It's possible that the update engine throws a generic error, such as upon seeing invalid
+ * payload properties (which come from OTA packages), or failing to set up the network
+ * with the given id.</p>
+ */
+ private void updateEngineApplyPayload(UpdateData update) {
+ Log.d(TAG, "updateEngineApplyPayload invoked with url " + update.mPayload.getUrl());
+
+ synchronized (mLock) {
+ mLastUpdateData = update;
+ }
+
+ ArrayList<String> properties = new ArrayList<>(update.getPayload().getProperties());
+ properties.addAll(update.getExtraProperties());
+
+ try {
+ mUpdateEngine.applyPayload(
+ update.getPayload().getUrl(),
+ update.getPayload().getOffset(),
+ update.getPayload().getSize(),
+ properties.toArray(new String[0]));
+ } catch (Exception e) {
+ Log.e(TAG, "UpdateEngine failed to apply the update", e);
+ setUpdaterStateSilent(UpdaterState.ERROR);
+ }
+ }
+
+ /**
+ * Re-applies {@link this.mLastUpdateData} to update_engine.
+ */
+ private void updateEngineReApplyPayload() {
+ Log.d(TAG, "updateEngineReApplyPayload invoked");
+ UpdateData lastUpdate;
+ synchronized (mLock) {
+ // mLastPayloadSpec might be empty in some cases.
+ // But to make this sample app simple, we will not handle it.
+ Preconditions.checkArgument(
+ mLastUpdateData != null,
+ "mLastUpdateData must be present.");
+ lastUpdate = mLastUpdateData;
+ }
+ updateEngineApplyPayload(lastUpdate);
+ }
+
+ /**
+ * Sets the new slot that has the updated partitions as the active slot,
+ * which device will boot into next time.
+ * This method is only supposed to be called after the payload is applied.
+ *
+ * Invoking {@link UpdateEngine#applyPayload} with the same payload url, offset, size
+ * and payload metadata headers doesn't trigger new update. It can be used to just switch
+ * active A/B slot.
+ *
+ * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
+ * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
+ */
+ public synchronized void setSwitchSlotOnReboot() {
+ Log.d(TAG, "setSwitchSlotOnReboot invoked");
+
+ // When mManualSwitchSlotRequired set false, next time
+ // onApplicationPayloadComplete is called,
+ // it will set updater state to REBOOT_REQUIRED.
+ mManualSwitchSlotRequired.set(false);
+
+ UpdateData.Builder builder;
+ synchronized (mLock) {
+ // To make sample app simple, we don't handle it.
+ Preconditions.checkArgument(
+ mLastUpdateData != null,
+ "mLastUpdateData must be present.");
+ builder = mLastUpdateData.toBuilder();
+ }
+ // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks.
+ builder.setExtraProperties(
+ Collections.singletonList(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL));
+ // UpdateEngine sets property SWITCH_SLOT_ON_REBOOT=1 by default.
+ // HTTP headers are not required, UpdateEngine is not expected to stream payload.
+ updateEngineApplyPayload(builder.build());
+ }
+
+ /**
+ * Synchronize UpdaterState with UpdateEngine status.
+ * Apply necessary UpdateEngine operation if status are out of sync.
+ *
+ * It's expected to be called once when sample app binds itself to UpdateEngine.
+ */
+ private void synchronizeUpdaterStateWithUpdateEngineStatus() {
+ Log.d(TAG, "synchronizeUpdaterStateWithUpdateEngineStatus is invoked.");
+
+ int state = mUpdaterState.get();
+ int engineStatus = mUpdateEngineStatus.get();
+
+ if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
+ // If update has been installed before running the sample app,
+ // set state to REBOOT_REQUIRED.
+ initializeUpdateState(UpdaterState.REBOOT_REQUIRED);
+ return;
+ }
+
+ switch (state) {
+ case UpdaterState.IDLE:
+ case UpdaterState.ERROR:
+ case UpdaterState.PAUSED:
+ case UpdaterState.SLOT_SWITCH_REQUIRED:
+ // It might happen when update is started not from the sample app.
+ // To make the sample app simple, we won't handle this case.
+ Preconditions.checkState(
+ engineStatus == UpdateEngine.UpdateStatusConstants.IDLE,
+ "When mUpdaterState is %s, mUpdateEngineStatus "
+ + "must be 0/IDLE, but it is %s",
+ state,
+ engineStatus);
+ break;
+ case UpdaterState.RUNNING:
+ if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
+ || engineStatus == UpdateEngine.UpdateStatusConstants.IDLE) {
+ Log.i(TAG, "ensureUpdateEngineStatusIsRunning - re-applying last payload");
+ // Re-apply latest update. It makes update_engine to invoke
+ // onPayloadApplicationComplete callback. The callback notifies
+ // if update was successful or not.
+ updateEngineReApplyPayload();
+ }
+ break;
+ case UpdaterState.REBOOT_REQUIRED:
+ // This might happen when update is installed by other means,
+ // and sample app is not aware of it.
+ // To make the sample app simple, we won't handle this case.
+ Preconditions.checkState(
+ engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT,
+ "When mUpdaterState is %s, mUpdateEngineStatus "
+ + "must be 6/UPDATED_NEED_REBOOT, but it is %s",
+ state,
+ engineStatus);
+ break;
+ default:
+ throw new IllegalStateException("This block should not be reached.");
+ }
+ }
+
+ /**
+ * Invoked by update_engine whenever update status or progress changes.
+ * It's also guaranteed to be invoked when app binds to the update_engine, except
+ * when update_engine fails to initialize (as defined in
+ * system/update_engine/binder_service_android.cc in
+ * function BinderUpdateEngineAndroidService::bind).
+ *
+ * @param status one of {@link UpdateEngine.UpdateStatusConstants}.
+ * @param progress a number from 0.0 to 1.0.
+ */
+ private void onStatusUpdate(int status, float progress) {
+ Log.d(TAG, String.format(
+ "onStatusUpdate invoked, status=%s, progress=%.2f",
+ status,
+ progress));
+
+ int previousStatus = mUpdateEngineStatus.get();
+ mUpdateEngineStatus.set(status);
+ mProgress.set(progress);
+
+ if (!mStateSynchronized.getAndSet(true)) {
+ // We synchronize state with engine status once
+ // only when sample app is bound to UpdateEngine.
+ synchronizeUpdaterStateWithUpdateEngineStatus();
+ }
+
+ getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(mProgress.get()));
+
+ if (previousStatus != status) {
+ getOnEngineStatusUpdateCallback().ifPresent(callback -> callback.accept(status));
+ }
+ }
+
+ private void onPayloadApplicationComplete(int errorCode) {
+ Log.d(TAG, "onPayloadApplicationComplete invoked, errorCode=" + errorCode);
+ mEngineErrorCode.set(errorCode);
+ if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
+ || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
+ setUpdaterStateSilent(isManualSwitchSlotRequired()
+ ? UpdaterState.SLOT_SWITCH_REQUIRED
+ : UpdaterState.REBOOT_REQUIRED);
+ } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
+ setUpdaterStateSilent(UpdaterState.ERROR);
+ }
+
+ getOnEngineCompleteCallback()
+ .ifPresent(callback -> callback.accept(errorCode));
+ }
+
+ /**
+ * Helper class to delegate {@code update_engine} callback invocations to UpdateManager.
+ */
+ class UpdateEngineCallbackImpl extends UpdateEngineCallback {
+ @Override
+ public void onStatusUpdate(int status, float percent) {
+ UpdateManager.this.onStatusUpdate(status, percent);
+ }
+
+ @Override
+ public void onPayloadApplicationComplete(int errorCode) {
+ UpdateManager.this.onPayloadApplicationComplete(errorCode);
+ }
+ }
+
+ /**
+ *
+ * Contains update data - PayloadSpec and extra properties list.
+ *
+ * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}.
+ * {@code mExtraProperties} is a list of additional properties to pass to
+ * {@link UpdateEngine#applyPayload}.</p>
+ */
+ private static class UpdateData {
+ private final PayloadSpec mPayload;
+ private final ImmutableList<String> mExtraProperties;
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ UpdateData(Builder builder) {
+ this.mPayload = builder.mPayload;
+ this.mExtraProperties = ImmutableList.copyOf(builder.mExtraProperties);
+ }
+
+ public PayloadSpec getPayload() {
+ return mPayload;
+ }
+
+ public ImmutableList<String> getExtraProperties() {
+ return mExtraProperties;
+ }
+
+ public Builder toBuilder() {
+ return builder()
+ .setPayload(mPayload)
+ .setExtraProperties(mExtraProperties);
+ }
+
+ static class Builder {
+ private PayloadSpec mPayload;
+ private List<String> mExtraProperties;
+
+ public Builder setPayload(PayloadSpec payload) {
+ this.mPayload = payload;
+ return this;
+ }
+
+ public Builder setExtraProperties(List<String> extraProperties) {
+ this.mExtraProperties = new ArrayList<>(extraProperties);
+ return this;
+ }
+
+ public Builder addExtraProperty(String property) {
+ if (this.mExtraProperties == null) {
+ this.mExtraProperties = new ArrayList<>();
+ }
+ this.mExtraProperties.add(property);
+ return this;
+ }
+
+ public UpdateData build() {
+ return new UpdateData(this);
+ }
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
new file mode 100644
index 0000000..4eb0b68
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import android.util.SparseArray;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Controls updater state.
+ */
+public class UpdaterState {
+
+ public static final int IDLE = 0;
+ public static final int ERROR = 1;
+ public static final int RUNNING = 2;
+ public static final int PAUSED = 3;
+ public static final int SLOT_SWITCH_REQUIRED = 4;
+ public static final int REBOOT_REQUIRED = 5;
+
+ private static final SparseArray<String> STATE_MAP = new SparseArray<>();
+
+ static {
+ STATE_MAP.put(0, "IDLE");
+ STATE_MAP.put(1, "ERROR");
+ STATE_MAP.put(2, "RUNNING");
+ STATE_MAP.put(3, "PAUSED");
+ STATE_MAP.put(4, "SLOT_SWITCH_REQUIRED");
+ STATE_MAP.put(5, "REBOOT_REQUIRED");
+ }
+
+ /**
+ * Allowed state transitions. It's a map: key is a state, value is a set of states that
+ * are allowed to transition to from key.
+ */
+ private static final ImmutableMap<Integer, ImmutableSet<Integer>> TRANSITIONS =
+ ImmutableMap.<Integer, ImmutableSet<Integer>>builder()
+ .put(IDLE, ImmutableSet.of(IDLE, ERROR, RUNNING))
+ .put(ERROR, ImmutableSet.of(IDLE))
+ .put(RUNNING, ImmutableSet.of(
+ IDLE, ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED))
+ .put(PAUSED, ImmutableSet.of(ERROR, RUNNING, IDLE))
+ .put(SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR, REBOOT_REQUIRED, IDLE))
+ .put(REBOOT_REQUIRED, ImmutableSet.of(IDLE))
+ .build();
+
+ private AtomicInteger mState;
+
+ public UpdaterState(int state) {
+ this.mState = new AtomicInteger(state);
+ }
+
+ /**
+ * Returns updater state.
+ */
+ public int get() {
+ return mState.get();
+ }
+
+ /**
+ * Sets the updater state.
+ *
+ * @throws InvalidTransitionException if transition is not allowed.
+ */
+ public void set(int newState) throws InvalidTransitionException {
+ int oldState = mState.get();
+ if (!TRANSITIONS.get(oldState).contains(newState)) {
+ throw new InvalidTransitionException(
+ "Can't transition from " + oldState + " to " + newState);
+ }
+ mState.set(newState);
+ }
+
+ /**
+ * Converts status code to status name.
+ */
+ public static String getStateText(int state) {
+ return STATE_MAP.get(state);
+ }
+
+ /**
+ * Defines invalid state transition exception.
+ */
+ public static class InvalidTransitionException extends Exception {
+ public InvalidTransitionException(String msg) {
+ super(msg);
+ }
+ }
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
new file mode 100644
index 0000000..9314048
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.services;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME;
+
+import android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RecoverySystem;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.util.FileDownloader;
+import com.example.android.systemupdatersample.util.PackageFiles;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.google.common.collect.ImmutableSet;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+/**
+ * This IntentService will download/extract the necessary files from the package zip
+ * without downloading the whole package. And it constructs {@link PayloadSpec}.
+ * All this work required to install streaming A/B updates.
+ *
+ * PrepareStreamingService 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 {
+
+ /**
+ * UpdateResultCallback result codes.
+ */
+ public static final int RESULT_CODE_SUCCESS = 0;
+ public static final int RESULT_CODE_ERROR = 1;
+
+ /**
+ * This interface is used to send results from {@link PrepareStreamingService} 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 payloadSpec prepared payload spec for streaming update
+ */
+ void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+ }
+
+ /**
+ * Starts PrepareStreamingService.
+ *
+ * @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,
+ UpdateResultCallback resultCallback) {
+ Log.d(TAG, "Starting PrepareStreamingService");
+ ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback);
+ Intent intent = new Intent(context, PrepareStreamingService.class);
+ intent.putExtra(EXTRA_PARAM_CONFIG, config);
+ intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+ context.startService(intent);
+ }
+
+ public PrepareStreamingService() {
+ 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";
+
+ /**
+ * The files that should be downloaded before streaming.
+ */
+ private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
+ ImmutableSet.of(
+ PackageFiles.CARE_MAP_FILE_NAME,
+ PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
+ PackageFiles.METADATA_FILE_NAME,
+ PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
+ );
+
+ private final PayloadSpecs mPayloadSpecs = new PayloadSpecs();
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ Log.d(TAG, "On handle intent is called");
+ UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG);
+ ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
+
+ try {
+ PayloadSpec spec = execute(config);
+ resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to prepare streaming update", e);
+ resultReceiver.send(RESULT_CODE_ERROR, null);
+ }
+ }
+
+ /**
+ * 1. Downloads files for streaming updates.
+ * 2. Makes sure required files are present.
+ * 3. Checks OTA package compatibility with the device.
+ * 4. Constructs {@link PayloadSpec} for streaming update.
+ */
+ private PayloadSpec execute(UpdateConfig config)
+ throws IOException, PreparationFailedException {
+
+ downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
+
+ Optional<UpdateConfig.PackageFile> payloadBinary =
+ UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
+
+ if (!payloadBinary.isPresent()) {
+ throw new PreparationFailedException(
+ "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
+ }
+
+ if (!UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config).isPresent()
+ || !Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile().exists()) {
+ throw new IOException(PAYLOAD_PROPERTIES_FILE_NAME + " not found");
+ }
+
+ File compatibilityFile = Paths.get(OTA_PACKAGE_DIR, COMPATIBILITY_ZIP_FILE_NAME).toFile();
+ if (compatibilityFile.isFile()) {
+ Log.i(TAG, "Verifying OTA package for compatibility with the device");
+ if (!verifyPackageCompatibility(compatibilityFile)) {
+ throw new PreparationFailedException(
+ "OTA package is not compatible with this device");
+ }
+ }
+
+ return mPayloadSpecs.forStreaming(config.getUrl(),
+ payloadBinary.get().getOffset(),
+ payloadBinary.get().getSize(),
+ Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile());
+ }
+
+ /**
+ * Downloads files defined in {@link UpdateConfig#getAbConfig()}
+ * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
+ * in directory {@code dir}.
+ * @throws IOException when can't download a file
+ */
+ private void downloadPreStreamingFiles(UpdateConfig config, String dir)
+ throws IOException {
+ Log.d(TAG, "Deleting existing files from " + dir);
+ for (String file : PRE_STREAMING_FILES_SET) {
+ Files.deleteIfExists(Paths.get(OTA_PACKAGE_DIR, file));
+ }
+ Log.d(TAG, "Downloading files to " + dir);
+ for (UpdateConfig.PackageFile file : config.getAbConfig().getPropertyFiles()) {
+ if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) {
+ Log.d(TAG, "Downloading file " + file.getFilename());
+ FileDownloader downloader = new FileDownloader(
+ config.getUrl(),
+ file.getOffset(),
+ file.getSize(),
+ Paths.get(dir, file.getFilename()).toFile());
+ downloader.download();
+ }
+ }
+ }
+
+ /**
+ * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME}
+ * @return true if OTA package is compatible with this device
+ */
+ private boolean verifyPackageCompatibility(File file) {
+ try {
+ return RecoverySystem.verifyPackageCompatibility(file);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to verify package compatibility", e);
+ return false;
+ }
+ }
+
+ /**
+ * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec}
+ * to {@link UpdateResultCallback#onReceiveResult}.
+ */
+ private static class CallbackResultReceiver extends ResultReceiver {
+
+ static Bundle createBundle(PayloadSpec payloadSpec) {
+ Bundle b = new Bundle();
+ b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec);
+ return b;
+ }
+
+ private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec";
+
+ private UpdateResultCallback mUpdateResultCallback;
+
+ CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
+ super(handler);
+ this.mUpdateResultCallback = updateResultCallback;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ PayloadSpec payloadSpec = null;
+ if (resultCode == RESULT_CODE_SUCCESS) {
+ payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC);
+ }
+ mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec);
+ }
+ }
+
+ private static class PreparationFailedException extends Exception {
+ PreparationFailedException(String message) {
+ super(message);
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
new file mode 100644
index 0000000..fc9fddd
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.graphics.Color;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.UpdateEngine;
+import android.util.Log;
+import android.view.View;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import com.example.android.systemupdatersample.R;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.UpdateManager;
+import com.example.android.systemupdatersample.UpdaterState;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
+import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
+
+import java.util.List;
+
+/**
+ * UI for SystemUpdaterSample app.
+ */
+public class MainActivity extends Activity {
+
+ private static final String TAG = "MainActivity";
+
+ private TextView mTextViewBuild;
+ private Spinner mSpinnerConfigs;
+ private TextView mTextViewConfigsDirHint;
+ private Button mButtonReload;
+ private Button mButtonApplyConfig;
+ private Button mButtonStop;
+ private Button mButtonReset;
+ private Button mButtonSuspend;
+ private Button mButtonResume;
+ private ProgressBar mProgressBar;
+ private TextView mTextViewUpdaterState;
+ private TextView mTextViewEngineStatus;
+ private TextView mTextViewEngineErrorCode;
+ private TextView mTextViewUpdateInfo;
+ private Button mButtonSwitchSlot;
+
+ private List<UpdateConfig> mConfigs;
+
+ private final UpdateManager mUpdateManager =
+ new UpdateManager(new UpdateEngine(), new PayloadSpecs());
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ this.mTextViewBuild = findViewById(R.id.textViewBuild);
+ this.mSpinnerConfigs = findViewById(R.id.spinnerConfigs);
+ this.mTextViewConfigsDirHint = findViewById(R.id.textViewConfigsDirHint);
+ this.mButtonReload = findViewById(R.id.buttonReload);
+ this.mButtonApplyConfig = findViewById(R.id.buttonApplyConfig);
+ this.mButtonStop = findViewById(R.id.buttonStop);
+ this.mButtonReset = findViewById(R.id.buttonReset);
+ this.mButtonSuspend = findViewById(R.id.buttonSuspend);
+ this.mButtonResume = findViewById(R.id.buttonResume);
+ this.mProgressBar = findViewById(R.id.progressBar);
+ this.mTextViewUpdaterState = findViewById(R.id.textViewUpdaterState);
+ this.mTextViewEngineStatus = findViewById(R.id.textViewEngineStatus);
+ this.mTextViewEngineErrorCode = findViewById(R.id.textViewEngineErrorCode);
+ this.mTextViewUpdateInfo = findViewById(R.id.textViewUpdateInfo);
+ this.mButtonSwitchSlot = findViewById(R.id.buttonSwitchSlot);
+
+ this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
+
+ uiResetWidgets();
+ loadUpdateConfigs();
+
+ this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange);
+ this.mUpdateManager.setOnEngineStatusUpdateCallback(this::onEngineStatusUpdate);
+ this.mUpdateManager.setOnEngineCompleteCallback(this::onEnginePayloadApplicationComplete);
+ this.mUpdateManager.setOnProgressUpdateCallback(this::onProgressUpdate);
+ }
+
+ @Override
+ protected void onDestroy() {
+ this.mUpdateManager.setOnEngineStatusUpdateCallback(null);
+ this.mUpdateManager.setOnProgressUpdateCallback(null);
+ this.mUpdateManager.setOnEngineCompleteCallback(null);
+ super.onDestroy();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ // Binding to UpdateEngine invokes onStatusUpdate callback,
+ // persisted updater state has to be loaded and prepared beforehand.
+ this.mUpdateManager.bind();
+ }
+
+ @Override
+ protected void onPause() {
+ this.mUpdateManager.unbind();
+ super.onPause();
+ }
+
+ /**
+ * reload button is clicked
+ */
+ public void onReloadClick(View view) {
+ loadUpdateConfigs();
+ }
+
+ /**
+ * view config button is clicked
+ */
+ public void onViewConfigClick(View view) {
+ UpdateConfig config = mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+ new AlertDialog.Builder(this)
+ .setTitle(config.getName())
+ .setMessage(config.getRawJson())
+ .setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss())
+ .show();
+ }
+
+ /**
+ * apply config button is clicked
+ */
+ public void onApplyConfigClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Apply Update")
+ .setMessage("Do you really want to apply this update?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ uiResetWidgets();
+ uiResetEngineText();
+ applyUpdate(getSelectedConfig());
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ private void applyUpdate(UpdateConfig config) {
+ try {
+ mUpdateManager.applyUpdate(this, config);
+ } catch (UpdaterState.InvalidTransitionException e) {
+ Log.e(TAG, "Failed to apply update " + config.getName(), e);
+ }
+ }
+
+ /**
+ * stop button clicked
+ */
+ public void onStopClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Stop Update")
+ .setMessage("Do you really want to cancel running update?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ cancelRunningUpdate();
+ })
+ .setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ private void cancelRunningUpdate() {
+ try {
+ mUpdateManager.cancelRunningUpdate();
+ } catch (UpdaterState.InvalidTransitionException e) {
+ Log.e(TAG, "Failed to cancel running update", e);
+ }
+ }
+
+ /**
+ * reset button clicked
+ */
+ public void onResetClick(View view) {
+ new AlertDialog.Builder(this)
+ .setTitle("Reset Update")
+ .setMessage("Do you really want to cancel running update"
+ + " and restore old version?")
+ .setIcon(android.R.drawable.ic_dialog_alert)
+ .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
+ resetUpdate();
+ })
+ .setNegativeButton(android.R.string.cancel, null).show();
+ }
+
+ private void resetUpdate() {
+ try {
+ mUpdateManager.resetUpdate();
+ } catch (UpdaterState.InvalidTransitionException e) {
+ Log.e(TAG, "Failed to reset update", e);
+ }
+ }
+
+ /**
+ * suspend button clicked
+ */
+ public void onSuspendClick(View view) {
+ try {
+ mUpdateManager.suspend();
+ } catch (UpdaterState.InvalidTransitionException e) {
+ Log.e(TAG, "Failed to suspend running update", e);
+ }
+ }
+
+ /**
+ * resume button clicked
+ */
+ public void onResumeClick(View view) {
+ try {
+ uiResetWidgets();
+ uiResetEngineText();
+ mUpdateManager.resume();
+ } catch (UpdaterState.InvalidTransitionException e) {
+ Log.e(TAG, "Failed to resume running update", e);
+ }
+ }
+
+ /**
+ * switch slot button clicked
+ */
+ public void onSwitchSlotClick(View view) {
+ uiResetWidgets();
+ mUpdateManager.setSwitchSlotOnReboot();
+ }
+
+ /**
+ * Invoked when SystemUpdaterSample app state changes.
+ * Value of {@code state} will be one of the
+ * values from {@link UpdaterState}.
+ */
+ private void onUpdaterStateChange(int state) {
+ Log.i(TAG, "UpdaterStateChange state="
+ + UpdaterState.getStateText(state)
+ + "/" + state);
+ runOnUiThread(() -> {
+ setUiUpdaterState(state);
+
+ if (state == UpdaterState.IDLE) {
+ uiStateIdle();
+ } else if (state == UpdaterState.RUNNING) {
+ uiStateRunning();
+ } else if (state == UpdaterState.PAUSED) {
+ uiStatePaused();
+ } else if (state == UpdaterState.ERROR) {
+ uiStateError();
+ } else if (state == UpdaterState.SLOT_SWITCH_REQUIRED) {
+ uiStateSlotSwitchRequired();
+ } else if (state == UpdaterState.REBOOT_REQUIRED) {
+ uiStateRebootRequired();
+ }
+ });
+ }
+
+ /**
+ * Invoked when {@link UpdateEngine} status changes. Value of {@code status} will
+ * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
+ */
+ private void onEngineStatusUpdate(int status) {
+ Log.i(TAG, "StatusUpdate - status="
+ + UpdateEngineStatuses.getStatusText(status)
+ + "/" + status);
+ runOnUiThread(() -> {
+ setUiEngineStatus(status);
+ });
+ }
+
+ /**
+ * Invoked when the payload has been applied, whether successfully or
+ * unsuccessfully. The value of {@code errorCode} will be one of the
+ * values from {@link UpdateEngine.ErrorCodeConstants}.
+ */
+ private void onEnginePayloadApplicationComplete(int errorCode) {
+ final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
+ ? "SUCCESS"
+ : "FAILURE";
+ Log.i(TAG,
+ "PayloadApplicationCompleted - errorCode="
+ + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ + " " + completionState);
+ runOnUiThread(() -> {
+ setUiEngineErrorCode(errorCode);
+ });
+ }
+
+ /**
+ * Invoked when update progress changes.
+ */
+ private void onProgressUpdate(double progress) {
+ mProgressBar.setProgress((int) (100 * progress));
+ }
+
+ /** resets ui */
+ private void uiResetWidgets() {
+ mTextViewBuild.setText(Build.DISPLAY);
+ mSpinnerConfigs.setEnabled(false);
+ mButtonReload.setEnabled(false);
+ mButtonApplyConfig.setEnabled(false);
+ mButtonStop.setEnabled(false);
+ mButtonReset.setEnabled(false);
+ mButtonSuspend.setEnabled(false);
+ mButtonResume.setEnabled(false);
+ mProgressBar.setEnabled(false);
+ mProgressBar.setVisibility(ProgressBar.INVISIBLE);
+ mButtonSwitchSlot.setEnabled(false);
+ mTextViewUpdateInfo.setTextColor(Color.parseColor("#aaaaaa"));
+ }
+
+ private void uiResetEngineText() {
+ mTextViewEngineStatus.setText(R.string.unknown);
+ mTextViewEngineErrorCode.setText(R.string.unknown);
+ // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies updater state properly.
+ }
+
+ private void uiStateIdle() {
+ uiResetWidgets();
+ mButtonReset.setEnabled(true);
+ mSpinnerConfigs.setEnabled(true);
+ mButtonReload.setEnabled(true);
+ mButtonApplyConfig.setEnabled(true);
+ mProgressBar.setProgress(0);
+ }
+
+ private void uiStateRunning() {
+ uiResetWidgets();
+ mProgressBar.setEnabled(true);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ mButtonStop.setEnabled(true);
+ mButtonSuspend.setEnabled(true);
+ }
+
+ private void uiStatePaused() {
+ uiResetWidgets();
+ mButtonReset.setEnabled(true);
+ mProgressBar.setEnabled(true);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ mButtonResume.setEnabled(true);
+ }
+
+ private void uiStateSlotSwitchRequired() {
+ uiResetWidgets();
+ mButtonReset.setEnabled(true);
+ mProgressBar.setEnabled(true);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ mButtonSwitchSlot.setEnabled(true);
+ mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777"));
+ }
+
+ private void uiStateError() {
+ uiResetWidgets();
+ mButtonReset.setEnabled(true);
+ mProgressBar.setEnabled(true);
+ mProgressBar.setVisibility(ProgressBar.VISIBLE);
+ }
+
+ private void uiStateRebootRequired() {
+ uiResetWidgets();
+ mButtonReset.setEnabled(true);
+ }
+
+ /**
+ * loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
+ */
+ private void loadUpdateConfigs() {
+ mConfigs = UpdateConfigs.getUpdateConfigs(this);
+ loadConfigsToSpinner(mConfigs);
+ }
+
+ /**
+ * @param status update engine status code
+ */
+ private void setUiEngineStatus(int status) {
+ String statusText = UpdateEngineStatuses.getStatusText(status);
+ mTextViewEngineStatus.setText(statusText + "/" + status);
+ }
+
+ /**
+ * @param errorCode update engine error code
+ */
+ private void setUiEngineErrorCode(int errorCode) {
+ String errorText = UpdateEngineErrorCodes.getCodeName(errorCode);
+ mTextViewEngineErrorCode.setText(errorText + "/" + errorCode);
+ }
+
+ /**
+ * @param state updater sample state
+ */
+ private void setUiUpdaterState(int state) {
+ String stateText = UpdaterState.getStateText(state);
+ mTextViewUpdaterState.setText(stateText + "/" + state);
+ }
+
+ private void loadConfigsToSpinner(List<UpdateConfig> configs) {
+ String[] spinnerArray = UpdateConfigs.configsToNames(configs);
+ ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this,
+ android.R.layout.simple_spinner_item,
+ spinnerArray);
+ spinnerArrayAdapter.setDropDownViewResource(android.R.layout
+ .simple_spinner_dropdown_item);
+ mSpinnerConfigs.setAdapter(spinnerArrayAdapter);
+ }
+
+ private UpdateConfig getSelectedConfig() {
+ return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
new file mode 100644
index 0000000..ddd0919
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+
+/**
+ * Downloads chunk of a file from given url using {@code offset} and {@code size},
+ * and saves to a given location.
+ *
+ * In real-life application this helper class should download from HTTP Server,
+ * but in this sample app it will only download from a local file.
+ */
+public final class FileDownloader {
+
+ private String mUrl;
+ private long mOffset;
+ private long mSize;
+ private File mDestination;
+
+ public FileDownloader(String url, long offset, long size, File destination) {
+ this.mUrl = url;
+ this.mOffset = offset;
+ this.mSize = size;
+ this.mDestination = destination;
+ }
+
+ /**
+ * Downloads the file with given offset and size.
+ * @throws IOException when can't download the file
+ */
+ public void download() throws IOException {
+ Log.d("FileDownloader", "downloading " + mDestination.getName()
+ + " from " + mUrl
+ + " to " + mDestination.getAbsolutePath());
+
+ URL url = new URL(mUrl);
+ URLConnection connection = url.openConnection();
+ connection.connect();
+
+ // download the file
+ try (InputStream input = connection.getInputStream()) {
+ try (OutputStream output = new FileOutputStream(mDestination)) {
+ long skipped = input.skip(mOffset);
+ if (skipped != mOffset) {
+ throw new IOException("Can't download file "
+ + mUrl
+ + " with given offset "
+ + mOffset);
+ }
+ byte[] data = new byte[4096];
+ long total = 0;
+ while (total < mSize) {
+ int needToRead = (int) Math.min(4096, mSize - total);
+ int count = input.read(data, 0, needToRead);
+ if (count <= 0) {
+ break;
+ }
+ output.write(data, 0, count);
+ total += count;
+ }
+ if (total != mSize) {
+ throw new IOException("Can't download file "
+ + mUrl
+ + " with given size "
+ + mSize);
+ }
+ }
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
new file mode 100644
index 0000000..b485234
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+/** Utility class for an OTA package. */
+public final class PackageFiles {
+
+ /**
+ * Directory used to perform updates.
+ */
+ public static final String OTA_PACKAGE_DIR = "/data/ota_package";
+
+ /**
+ * update payload, it will be passed to {@code UpdateEngine#applyPayload}.
+ */
+ public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
+
+ /**
+ * Currently, when calling {@code UpdateEngine#applyPayload} to perform actions
+ * that don't require network access (e.g. change slot), update_engine still
+ * talks to the server to download/verify file.
+ * {@code update_engine} might throw error when rebooting if {@code UpdateEngine#applyPayload}
+ * is not supplied right headers and tokens.
+ * This behavior might change in future android versions.
+ *
+ * To avoid extra network request in {@code update_engine}, this file has to be
+ * downloaded and put in {@code OTA_PACKAGE_DIR}.
+ */
+ public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
+
+ public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
+
+ /** The zip entry in an A/B OTA package, which will be used by update_verifier. */
+ public static final String CARE_MAP_FILE_NAME = "care_map.txt";
+
+ public static final String METADATA_FILE_NAME = "metadata";
+
+ /**
+ * The zip file that claims the compatibility of the update package to check against the Android
+ * framework to ensure that the package can be installed on the device.
+ */
+ public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
+
+ private PackageFiles() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
new file mode 100644
index 0000000..f062317
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/** The helper class that creates {@link PayloadSpec}. */
+public class PayloadSpecs {
+
+ public PayloadSpecs() {}
+
+ /**
+ * The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package
+ * format. We want to find out the offset of the entry, so that we can pass it over to the A/B
+ * updater without making an extra copy of the payload.
+ *
+ * <p>According to Android docs, the entries are listed in the order in which they appear in the
+ * zip file. So we enumerate the entries to identify the offset of the payload file.
+ * http://developer.android.com/reference/java/util/zip/ZipFile.html#entries()
+ */
+ public PayloadSpec forNonStreaming(File packageFile) throws IOException {
+ boolean payloadFound = false;
+ long payloadOffset = 0;
+ long payloadSize = 0;
+
+ List<String> properties = new ArrayList<>();
+ try (ZipFile zip = new ZipFile(packageFile)) {
+ Enumeration<? extends ZipEntry> entries = zip.entries();
+ long offset = 0;
+ while (entries.hasMoreElements()) {
+ ZipEntry entry = entries.nextElement();
+ String name = entry.getName();
+ // Zip local file header has 30 bytes + filename + sizeof extra field.
+ // https://en.wikipedia.org/wiki/Zip_(file_format)
+ long extraSize = entry.getExtra() == null ? 0 : entry.getExtra().length;
+ offset += 30 + name.length() + extraSize;
+
+ if (entry.isDirectory()) {
+ continue;
+ }
+
+ long length = entry.getCompressedSize();
+ if (PackageFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
+ if (entry.getMethod() != ZipEntry.STORED) {
+ throw new IOException("Invalid compression method.");
+ }
+ payloadFound = true;
+ payloadOffset = offset;
+ payloadSize = length;
+ } else if (PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
+ InputStream inputStream = zip.getInputStream(entry);
+ if (inputStream != null) {
+ BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
+ String line;
+ while ((line = br.readLine()) != null) {
+ properties.add(line);
+ }
+ }
+ }
+ offset += length;
+ }
+ }
+
+ if (!payloadFound) {
+ throw new IOException("Failed to find payload entry in the given package.");
+ }
+ return PayloadSpec.newBuilder()
+ .url("file://" + packageFile.getAbsolutePath())
+ .offset(payloadOffset)
+ .size(payloadSize)
+ .properties(properties)
+ .build();
+ }
+
+ /**
+ * Creates a {@link PayloadSpec} for streaming update.
+ */
+ public PayloadSpec forStreaming(String updateUrl,
+ long offset,
+ long size,
+ File propertiesFile) throws IOException {
+ return PayloadSpec.newBuilder()
+ .url(updateUrl)
+ .offset(offset)
+ .size(size)
+ .properties(Files.readAllLines(propertiesFile.toPath()))
+ .build();
+ }
+
+ /**
+ * Converts an {@link PayloadSpec} to a string.
+ */
+ public String specToString(PayloadSpec payloadSpec) {
+ return "<PayloadSpec url=" + payloadSpec.getUrl()
+ + ", offset=" + payloadSpec.getOffset()
+ + ", size=" + payloadSpec.getSize()
+ + ", properties=" + Arrays.toString(
+ payloadSpec.getProperties().toArray(new String[0]))
+ + ">";
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
new file mode 100644
index 0000000..bbefcaf
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Utility class for working with json update configurations.
+ */
+public final class UpdateConfigs {
+
+ public static final String UPDATE_CONFIGS_ROOT = "configs/";
+
+ /**
+ * @param configs update configs
+ * @return list of names
+ */
+ public static String[] configsToNames(List<UpdateConfig> configs) {
+ return configs.stream().map(UpdateConfig::getName).toArray(String[]::new);
+ }
+
+ /**
+ * @param context app context
+ * @return configs root directory
+ */
+ public static String getConfigsRoot(Context context) {
+ return Paths
+ .get(context.getFilesDir().toString(), UPDATE_CONFIGS_ROOT)
+ .toString();
+ }
+
+ /**
+ * @param context application context
+ * @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
+ */
+ public static List<UpdateConfig> getUpdateConfigs(Context context) {
+ File root = new File(getConfigsRoot(context));
+ ArrayList<UpdateConfig> configs = new ArrayList<>();
+ if (!root.exists()) {
+ return configs;
+ }
+ for (final File f : root.listFiles()) {
+ if (!f.isDirectory() && f.getName().endsWith(".json")) {
+ try {
+ String json = new String(Files.readAllBytes(f.toPath()),
+ StandardCharsets.UTF_8);
+ configs.add(UpdateConfig.fromJson(json));
+ } catch (Exception e) {
+ Log.e("UpdateConfigs", "Can't read/parse config file " + f.getName(), e);
+ throw new RuntimeException(
+ "Can't read/parse config file " + f.getName(), e);
+ }
+ }
+ }
+ return configs;
+ }
+
+ /**
+ * @param filename searches by given filename
+ * @param config searches in {@link UpdateConfig#getAbConfig()}
+ * @return offset and size of {@code filename} in the package zip file
+ * stored as {@link UpdateConfig.PackageFile}.
+ */
+ public static Optional<UpdateConfig.PackageFile> getPropertyFile(
+ final String filename,
+ UpdateConfig config) {
+ return Arrays
+ .stream(config.getAbConfig().getPropertyFiles())
+ .filter(file -> filename.equals(file.getFilename()))
+ .findFirst();
+ }
+
+ private UpdateConfigs() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
new file mode 100644
index 0000000..13df88b
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.os.UpdateEngine;
+import android.util.SparseArray;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in {@link UpdateEngine.ErrorCodeConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineErrorCodes {
+
+ /**
+ * Error code from the update engine. Values must agree with the ones in
+ * system/update_engine/common/error_code.h.
+ */
+ public static final int UNKNOWN = -1;
+ public static final int UPDATED_BUT_NOT_ACTIVE = 52;
+ public static final int USER_CANCELLED = 48;
+
+ private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
+
+ static {
+ CODE_TO_NAME_MAP.put(0, "SUCCESS");
+ CODE_TO_NAME_MAP.put(1, "ERROR");
+ CODE_TO_NAME_MAP.put(4, "FILESYSTEM_COPIER_ERROR");
+ CODE_TO_NAME_MAP.put(5, "POST_INSTALL_RUNNER_ERROR");
+ CODE_TO_NAME_MAP.put(6, "PAYLOAD_MISMATCHED_TYPE_ERROR");
+ CODE_TO_NAME_MAP.put(7, "INSTALL_DEVICE_OPEN_ERROR");
+ CODE_TO_NAME_MAP.put(8, "KERNEL_DEVICE_OPEN_ERROR");
+ CODE_TO_NAME_MAP.put(9, "DOWNLOAD_TRANSFER_ERROR");
+ CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
+ CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
+ CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
+ CODE_TO_NAME_MAP.put(15, "NEW_ROOTFS_VERIFICATION_ERROR");
+ CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
+ CODE_TO_NAME_MAP.put(26, "DOWNLOAD_METADATA_SIGNATURE_MISMATCH");
+ CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
+ CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
+ }
+
+ /**
+ * Completion codes returned by update engine indicating that the update
+ * was successfully applied.
+ */
+ private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<>(
+ Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
+ // UPDATED_BUT_NOT_ACTIVE is returned when the payload is
+ // successfully applied but the
+ // device won't switch to the new slot after the next boot.
+ UPDATED_BUT_NOT_ACTIVE));
+
+ /**
+ * checks if update succeeded using errorCode
+ */
+ public static boolean isUpdateSucceeded(int errorCode) {
+ return SUCCEEDED_COMPLETION_CODES.contains(errorCode);
+ }
+
+ /**
+ * converts error code to error name
+ */
+ public static String getCodeName(int errorCode) {
+ return CODE_TO_NAME_MAP.get(errorCode);
+ }
+
+ private UpdateEngineErrorCodes() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
new file mode 100644
index 0000000..e368f14
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineProperties.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+/**
+ * Utility class for properties that will be passed to {@code UpdateEngine#applyPayload}.
+ */
+public final class UpdateEngineProperties {
+
+ /**
+ * The property indicating that the update engine should not switch slot
+ * when the device reboots.
+ */
+ public static final String PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT = "SWITCH_SLOT_ON_REBOOT=0";
+
+ /**
+ * The property to skip post-installation.
+ * https://source.android.com/devices/tech/ota/ab/#post-installation
+ */
+ public static final String PROPERTY_SKIP_POST_INSTALL = "RUN_POST_INSTALL=0";
+
+ private UpdateEngineProperties() {}
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
new file mode 100644
index 0000000..a96f19d
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import android.util.SparseArray;
+
+/**
+ * Helper class to work with update_engine's error codes.
+ * Many error codes are defined in {@code UpdateEngine.UpdateStatusConstants},
+ * but you can find more in system/update_engine/common/error_code.h.
+ */
+public final class UpdateEngineStatuses {
+
+ private static final SparseArray<String> STATUS_MAP = new SparseArray<>();
+
+ static {
+ STATUS_MAP.put(0, "IDLE");
+ STATUS_MAP.put(1, "CHECKING_FOR_UPDATE");
+ STATUS_MAP.put(2, "UPDATE_AVAILABLE");
+ STATUS_MAP.put(3, "DOWNLOADING");
+ STATUS_MAP.put(4, "VERIFYING");
+ STATUS_MAP.put(5, "FINALIZING");
+ STATUS_MAP.put(6, "UPDATED_NEED_REBOOT");
+ STATUS_MAP.put(7, "REPORTING_ERROR_EVENT");
+ STATUS_MAP.put(8, "ATTEMPTING_ROLLBACK");
+ STATUS_MAP.put(9, "DISABLED");
+ }
+
+ /**
+ * converts status code to status name
+ */
+ public static String getStatusText(int status) {
+ return STATUS_MAP.get(status);
+ }
+
+ private UpdateEngineStatuses() {}
+}
diff --git a/updater_sample/tests/Android.bp b/updater_sample/tests/Android.bp
new file mode 100644
index 0000000..c2783ef
--- /dev/null
+++ b/updater_sample/tests/Android.bp
@@ -0,0 +1,40 @@
+// Copyright (C) 2018 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+android_test {
+ name: "SystemUpdaterSampleTests",
+ sdk_version: "system_current",
+
+ libs: [
+ "android.test.base.stubs",
+ "android.test.runner.stubs",
+ "SystemUpdaterSample",
+ ],
+
+ static_libs: [
+ "android-support-test",
+ "mockito-target-minus-junit4",
+ "guava",
+ ],
+
+ instrumentation_for: "com.example.android.systemupdatersample",
+
+ optimize: {
+ enabled: false,
+ },
+
+ resource_dirs: ["res"],
+
+ srcs: ["src/**/*.java"],
+}
diff --git a/updater_sample/tests/AndroidManifest.xml b/updater_sample/tests/AndroidManifest.xml
new file mode 100644
index 0000000..76af5f1
--- /dev/null
+++ b/updater_sample/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.example.android.systemupdatersample.tests">
+
+ <uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
+
+ <!-- We add an application tag here just so that we can indicate that
+ this package needs to link against the android.test library,
+ which is needed when building test cases. -->
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.example.android.systemupdatersample"
+ android:label="Tests for SystemUpdaterSample."/>
+
+</manifest>
diff --git a/updater_sample/tests/build.properties b/updater_sample/tests/build.properties
new file mode 100644
index 0000000..e0c39de
--- /dev/null
+++ b/updater_sample/tests/build.properties
@@ -0,0 +1 @@
+tested.project.dir=..
diff --git a/updater_sample/tests/res/raw/ota_002_package.zip b/updater_sample/tests/res/raw/ota_002_package.zip
new file mode 100644
index 0000000..6bf2a23
--- /dev/null
+++ b/updater_sample/tests/res/raw/ota_002_package.zip
Binary files differ
diff --git a/updater_sample/tests/res/raw/update_config_001_stream.json b/updater_sample/tests/res/raw/update_config_001_stream.json
new file mode 100644
index 0000000..b024ad9
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_001_stream.json
@@ -0,0 +1,16 @@
+{
+ "name": "streaming-001",
+ "url": "http://foo.bar/update.zip",
+ "ab_install_type": "STREAMING",
+ "ab_config": {
+ "verify_payload_metadata": true,
+ "force_switch_slot": true,
+ "property_files": [
+ {
+ "filename": "payload.bin",
+ "offset": 195,
+ "size": 8
+ }
+ ]
+ }
+}
diff --git a/updater_sample/tests/res/raw/update_config_002_stream.json b/updater_sample/tests/res/raw/update_config_002_stream.json
new file mode 100644
index 0000000..12c18bb
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_002_stream.json
@@ -0,0 +1,42 @@
+{
+ "__": "*** Generated using tools/gen_update_config.py ***",
+ "ab_config": {
+ "verify_payload_metadata": true,
+ "force_switch_slot": false,
+ "property_files": [
+ {
+ "filename": "payload_metadata.bin",
+ "offset": 41,
+ "size": 827
+ },
+ {
+ "filename": "payload.bin",
+ "offset": 41,
+ "size": 1392
+ },
+ {
+ "filename": "payload_properties.txt",
+ "offset": 1485,
+ "size": 147
+ },
+ {
+ "filename": "care_map.txt",
+ "offset": 1674,
+ "size": 12
+ },
+ {
+ "filename": "compatibility.zip",
+ "offset": 1733,
+ "size": 17
+ },
+ {
+ "filename": "metadata",
+ "offset": 1809,
+ "size": 29
+ }
+ ]
+ },
+ "ab_install_type": "STREAMING",
+ "name": "S ota_002_package",
+ "url": "file:///data/my-sample-ota-builds-dir/ota_002_package.zip"
+}
\ No newline at end of file
diff --git a/updater_sample/tests/res/raw/update_config_003_nonstream.json b/updater_sample/tests/res/raw/update_config_003_nonstream.json
new file mode 100644
index 0000000..2011f76
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_003_nonstream.json
@@ -0,0 +1,17 @@
+{
+ "__": "*** Generated using tools/gen_update_config.py ***",
+ "ab_config": {
+ "verify_payload_metadata": true,
+ "force_switch_slot": false,
+ "property_files": [
+ {
+ "filename": "payload.bin",
+ "offset": 195,
+ "size": 8
+ }
+ ]
+ },
+ "ab_install_type": "NON_STREAMING",
+ "name": "S ota_002_package",
+ "url": "file:///data/my-sample-ota-builds-dir/ota_003_package.zip"
+}
\ No newline at end of file
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
new file mode 100644
index 0000000..48d0e42
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+import com.google.common.io.CharStreams;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Tests for {@link UpdateConfig}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigTest {
+
+ private static final String JSON_NON_STREAMING = "{"
+ + " \"name\": \"vip update\", \"url\": \"file:///my-builds/a.zip\","
+ + " \"ab_install_type\": \"NON_STREAMING\","
+ + " \"ab_config\": {"
+ + " \"force_switch_slot\": false,"
+ + " \"verify_payload_metadata\": false } }";
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private Context mContext;
+ private Context mTargetContext;
+ private String mJsonStreaming001;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ mJsonStreaming001 = readResource(R.raw.update_config_001_stream);
+ }
+
+ @Test
+ public void fromJson_parsesNonStreaming() throws Exception {
+ UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
+ assertEquals("name is parsed", "vip update", config.getName());
+ assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
+ assertSame("type is parsed",
+ UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
+ config.getInstallType());
+ assertEquals("url is parsed", "file:///my-builds/a.zip", config.getUrl());
+ }
+
+ @Test
+ public void fromJson_parsesStreaming() throws Exception {
+ UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
+ assertEquals("streaming-001", config.getName());
+ assertEquals("http://foo.bar/update.zip", config.getUrl());
+ assertSame(UpdateConfig.AB_INSTALL_TYPE_STREAMING, config.getInstallType());
+ assertEquals("payload.bin",
+ config.getAbConfig().getPropertyFiles()[0].getFilename());
+ assertEquals(195, config.getAbConfig().getPropertyFiles()[0].getOffset());
+ assertEquals(8, config.getAbConfig().getPropertyFiles()[0].getSize());
+ assertTrue(config.getAbConfig().getForceSwitchSlot());
+ }
+
+ @Test
+ public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
+ UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
+ thrown.expect(RuntimeException.class);
+ config.getUpdatePackageFile();
+ }
+
+ @Test
+ public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
+ String json = "{"
+ + " \"name\": \"upd\", \"url\": \"http://foo.bar\","
+ + " \"ab_install_type\": \"NON_STREAMING\","
+ + " \"ab_config\": {"
+ + " \"force_switch_slot\": false,"
+ + " \"verify_payload_metadata\": false } }";
+ UpdateConfig config = UpdateConfig.fromJson(json);
+ thrown.expect(RuntimeException.class);
+ config.getUpdatePackageFile();
+ }
+
+ @Test
+ public void getUpdatePackageFile_works() throws Exception {
+ UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
+ assertEquals("/my-builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
+ }
+
+ private String readResource(int id) throws IOException {
+ return CharStreams.toString(new InputStreamReader(
+ mContext.getResources().openRawResource(id)));
+ }
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
new file mode 100644
index 0000000..e05ad29
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.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.os.UpdateEngine;
+import android.os.UpdateEngineCallback;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CharStreams;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+
+/**
+ * Tests for {@link UpdateManager}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateManagerTest {
+
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private UpdateEngine mUpdateEngine;
+ @Mock
+ private PayloadSpecs mPayloadSpecs;
+ private UpdateManager mSubject;
+ private Context mContext;
+ private UpdateConfig mNonStreamingUpdate003;
+
+ @Before
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getContext();
+ mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs);
+ mNonStreamingUpdate003 =
+ UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream));
+ }
+
+ @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);
+
+ verify(mUpdateEngine).applyPayload(
+ "file://blah",
+ 120,
+ 340,
+ new String[] {
+ "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
+ });
+ }
+
+ @Test
+ public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() 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);
+ 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
+ });
+ }
+
+ private PayloadSpec buildMockPayloadSpec() {
+ PayloadSpec payload = mock(PayloadSpec.class);
+ when(payload.getUrl()).thenReturn("file://blah");
+ when(payload.getOffset()).thenReturn(120L);
+ when(payload.getSize()).thenReturn(340L);
+ when(payload.getProperties()).thenReturn(ImmutableList.of());
+ return payload;
+ }
+
+ private String readResource(int id) throws IOException {
+ return CharStreams.toString(new InputStreamReader(
+ mContext.getResources().openRawResource(id)));
+ }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
new file mode 100644
index 0000000..a136ff0
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.tests.R;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+/**
+ * Tests for {@link FileDownloader}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class FileDownloaderTest {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ private Context mTestContext;
+ private Context mTargetContext;
+
+ @Before
+ public void setUp() {
+ mTestContext = InstrumentationRegistry.getContext();
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @Test
+ public void download_downloadsChunkOfZip() throws Exception {
+ // Prepare the target file
+ File packageFile = Paths
+ .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
+ .toFile();
+ Files.deleteIfExists(packageFile.toPath());
+ Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
+ packageFile.toPath());
+ String url = "file://" + packageFile.getAbsolutePath();
+ // prepare where to download
+ File outFile = Paths
+ .get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt")
+ .toFile();
+ Files.deleteIfExists(outFile.toPath());
+ // download a chunk of ota.zip
+ FileDownloader downloader = new FileDownloader(url, 1674, 12, outFile);
+ downloader.download();
+ String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath()));
+ // archive contains text files with uppercase filenames
+ assertEquals("CARE_MAP-TXT", downloadedContent);
+ }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
new file mode 100644
index 0000000..0308693
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.tests.R;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Paths;
+
+/**
+ * Tests if PayloadSpecs parses update package zip file correctly.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class PayloadSpecsTest {
+
+ private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
+
+ private File mTestDir;
+
+ private Context mTargetContext;
+ private Context mTestContext;
+
+ private PayloadSpecs mPayloadSpecs;
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Before
+ public void setUp() {
+ mTargetContext = InstrumentationRegistry.getTargetContext();
+ mTestContext = InstrumentationRegistry.getContext();
+
+ mTestDir = mTargetContext.getCacheDir();
+ mPayloadSpecs = new PayloadSpecs();
+ }
+
+ @Test
+ public void forNonStreaming_works() throws Exception {
+ // Prepare the target file
+ File packageFile = Paths
+ .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
+ .toFile();
+ java.nio.file.Files.deleteIfExists(packageFile.toPath());
+ java.nio.file.Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
+ packageFile.toPath());
+ PayloadSpec spec = mPayloadSpecs.forNonStreaming(packageFile);
+
+ assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
+ assertEquals("correct payload offset",
+ 30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
+ assertEquals("correct payload size", 1392, spec.getSize());
+ assertEquals(4, spec.getProperties().size());
+ assertEquals(
+ "FILE_HASH=sEAK/NMbU7GGe01xt55FsPafIPk8IYyBOAd6SiDpiMs=",
+ spec.getProperties().get(0));
+ }
+
+ @Test
+ public void forNonStreaming_IOException() throws Exception {
+ thrown.expect(IOException.class);
+ mPayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
+ }
+
+ @Test
+ public void forStreaming_works() throws Exception {
+ String url = "http://a.com/b.zip";
+ long offset = 45;
+ long size = 200;
+ File propertiesFile = createMockPropertiesFile();
+
+ PayloadSpec spec = mPayloadSpecs.forStreaming(url, offset, size, propertiesFile);
+ assertEquals("same url", url, spec.getUrl());
+ assertEquals("same offset", offset, spec.getOffset());
+ assertEquals("same size", size, spec.getSize());
+ assertArrayEquals("correct properties",
+ new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
+ }
+
+ private File createMockPropertiesFile() throws IOException {
+ File propertiesFile = new File(mTestDir, PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME);
+ Files.asCharSink(propertiesFile, Charsets.UTF_8).write(PROPERTIES_CONTENTS);
+ return propertiesFile;
+ }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
new file mode 100644
index 0000000..4ccae93
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.example.android.systemupdatersample.UpdateConfig;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Tests for {@link UpdateConfigs}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class UpdateConfigsTest {
+
+ @Rule
+ public final ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void configsToNames_extractsNames() {
+ List<UpdateConfig> configs = Arrays.asList(
+ new UpdateConfig("blah", "http://", UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING),
+ new UpdateConfig("blah 2", "http://", UpdateConfig.AB_INSTALL_TYPE_STREAMING)
+ );
+ String[] names = UpdateConfigs.configsToNames(configs);
+ assertArrayEquals(new String[] {"blah", "blah 2"}, names);
+ }
+}
diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py
new file mode 100755
index 0000000..f2cb1a8
--- /dev/null
+++ b/updater_sample/tools/gen_update_config.py
@@ -0,0 +1,171 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Given a OTA package file, produces update config JSON file.
+
+Example:
+ $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+ bootable/recovery/updater_sample/tools/gen_update_config.py \\
+ --ab_install_type=STREAMING \\
+ ota-build-001.zip \\
+ my-config-001.json \\
+ http://foo.bar/ota-builds/ota-build-001.zip
+"""
+
+import argparse
+import json
+import os.path
+import sys
+import zipfile
+
+import ota_from_target_files # pylint: disable=import-error
+
+
+class GenUpdateConfig(object):
+ """
+ A class that generates update configuration file from an OTA package.
+
+ Currently supports only A/B (seamless) OTA packages.
+ TODO: add non-A/B packages support.
+ """
+
+ AB_INSTALL_TYPE_STREAMING = 'STREAMING'
+ AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
+
+ def __init__(self,
+ package,
+ url,
+ ab_install_type,
+ ab_force_switch_slot,
+ ab_verify_payload_metadata):
+ self.package = package
+ self.url = url
+ self.ab_install_type = ab_install_type
+ self.ab_force_switch_slot = ab_force_switch_slot
+ self.ab_verify_payload_metadata = ab_verify_payload_metadata
+ self.streaming_required = (
+ # payload.bin and payload_properties.txt must exist.
+ 'payload.bin',
+ 'payload_properties.txt',
+ )
+ self.streaming_optional = (
+ # care_map.txt is available only if dm-verity is enabled.
+ 'care_map.txt',
+ # compatibility.zip is available only if target supports Treble.
+ 'compatibility.zip',
+ )
+ self._config = None
+
+ @property
+ def config(self):
+ """Returns generated config object."""
+ return self._config
+
+ def run(self):
+ """Generates config."""
+ self._config = {
+ '__': '*** Generated using tools/gen_update_config.py ***',
+ 'name': self.ab_install_type[0] + ' ' + os.path.basename(self.package)[:-4],
+ 'url': self.url,
+ 'ab_config': self._gen_ab_config(),
+ 'ab_install_type': self.ab_install_type,
+ }
+
+ def _gen_ab_config(self):
+ """Builds config required for A/B update."""
+ with zipfile.ZipFile(self.package, 'r') as package_zip:
+ config = {
+ 'property_files': self._get_property_files(package_zip),
+ 'verify_payload_metadata': self.ab_verify_payload_metadata,
+ 'force_switch_slot': self.ab_force_switch_slot,
+ }
+
+ return config
+
+ @staticmethod
+ def _get_property_files(package_zip):
+ """Constructs the property-files list for A/B streaming metadata."""
+
+ ab_ota = ota_from_target_files.AbOtaPropertyFiles()
+ property_str = ab_ota.GetPropertyFilesString(package_zip, False)
+ property_files = []
+ for file in property_str.split(','):
+ filename, offset, size = file.split(':')
+ inner_file = {
+ 'filename': filename,
+ 'offset': int(offset),
+ 'size': int(size)
+ }
+ property_files.append(inner_file)
+
+ return property_files
+
+ def write(self, out):
+ """Writes config to the output file."""
+ with open(out, 'w') as out_file:
+ json.dump(self.config, out_file, indent=4, separators=(',', ': '), sort_keys=True)
+
+
+def main(): # pylint: disable=missing-docstring
+ ab_install_type_choices = [
+ GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
+ GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING]
+ parser = argparse.ArgumentParser(description=__doc__,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument('--ab_install_type',
+ type=str,
+ default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
+ choices=ab_install_type_choices,
+ help='A/B update installation type')
+ parser.add_argument('--ab_force_switch_slot',
+ default=False,
+ action='store_true',
+ help='if set device will boot to a new slot, otherwise user '
+ 'manually switches slot on the screen')
+ parser.add_argument('--ab_verify_payload_metadata',
+ default=False,
+ action='store_true',
+ help='if set the app will verify the update payload metadata using '
+ 'update_engine before downloading the whole package.')
+ parser.add_argument('package',
+ type=str,
+ help='OTA package zip file')
+ parser.add_argument('out',
+ type=str,
+ help='Update configuration JSON file')
+ parser.add_argument('url',
+ type=str,
+ help='OTA package download url')
+ args = parser.parse_args()
+
+ if not args.out.endswith('.json'):
+ print('out must be a json file')
+ sys.exit(1)
+
+ gen = GenUpdateConfig(
+ package=args.package,
+ url=args.url,
+ ab_install_type=args.ab_install_type,
+ ab_force_switch_slot=args.ab_force_switch_slot,
+ ab_verify_payload_metadata=args.ab_verify_payload_metadata)
+ gen.run()
+ gen.write(args.out)
+ print('Config is written to ' + args.out)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/updater_sample/tools/test_gen_update_config.py b/updater_sample/tools/test_gen_update_config.py
new file mode 100755
index 0000000..8b77cb2
--- /dev/null
+++ b/updater_sample/tools/test_gen_update_config.py
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Tests gen_update_config.py.
+
+Example:
+ $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+ python3 -m unittest test_gen_update_config
+"""
+
+import os.path
+import unittest
+from gen_update_config import GenUpdateConfig
+
+
+class GenUpdateConfigTest(unittest.TestCase): # pylint: disable=missing-docstring
+
+ def test_ab_install_type_streaming(self):
+ """tests if streaming property files' offset and size are generated properly"""
+ config, package = self._generate_config()
+ property_files = config['ab_config']['property_files']
+ self.assertEqual(len(property_files), 6)
+ with open(package, 'rb') as pkg_file:
+ for prop in property_files:
+ filename, offset, size = prop['filename'], prop['offset'], prop['size']
+ pkg_file.seek(offset)
+ raw_data = pkg_file.read(size)
+ if filename in ['payload.bin', 'payload_metadata.bin']:
+ pass
+ elif filename == 'payload_properties.txt':
+ pass
+ elif filename == 'metadata':
+ self.assertEqual(raw_data.decode('ascii'), 'META-INF/COM/ANDROID/METADATA')
+ else:
+ expected_data = filename.replace('.', '-').upper()
+ self.assertEqual(raw_data.decode('ascii'), expected_data)
+
+ @staticmethod
+ def _generate_config():
+ """Generates JSON config from ota_002_package.zip."""
+ ota_package = os.path.join(os.path.dirname(__file__),
+ '../tests/res/raw/ota_002_package.zip')
+ gen = GenUpdateConfig(ota_package,
+ 'file:///foo.bar',
+ GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
+ True, # ab_force_switch_slot
+ True) # ab_verify_payload_metadata
+ gen.run()
+ return gen.config, ota_package
diff --git a/vr_ui.cpp b/vr_ui.cpp
index a58c99e..a131a27 100644
--- a/vr_ui.cpp
+++ b/vr_ui.cpp
@@ -16,9 +16,15 @@
#include "vr_ui.h"
-#include <minui/minui.h>
+#include <android-base/properties.h>
-VrRecoveryUI::VrRecoveryUI() : kStereoOffset(RECOVERY_UI_VR_STEREO_OFFSET) {}
+#include "minui/minui.h"
+
+constexpr int kDefaultStereoOffset = 0;
+
+VrRecoveryUI::VrRecoveryUI()
+ : stereo_offset_(
+ android::base::GetIntProperty("ro.recovery.ui.stereo_offset", kDefaultStereoOffset)) {}
int VrRecoveryUI::ScreenWidth() const {
return gr_fb_width() / 2;
@@ -30,36 +36,37 @@
void VrRecoveryUI::DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx,
int dy) const {
- gr_blit(surface, sx, sy, w, h, dx + kStereoOffset, dy);
- gr_blit(surface, sx, sy, w, h, dx - kStereoOffset + ScreenWidth(), dy);
+ gr_blit(surface, sx, sy, w, h, dx + stereo_offset_, dy);
+ gr_blit(surface, sx, sy, w, h, dx - stereo_offset_ + ScreenWidth(), dy);
}
void VrRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
- gr_texticon(x + kStereoOffset, y, surface);
- gr_texticon(x - kStereoOffset + ScreenWidth(), y, surface);
+ gr_texticon(x + stereo_offset_, y, surface);
+ gr_texticon(x - stereo_offset_ + ScreenWidth(), y, surface);
}
-int VrRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
- gr_text(gr_sys_font(), x + kStereoOffset, y, line, bold);
- gr_text(gr_sys_font(), x - kStereoOffset + ScreenWidth(), y, line, bold);
+int VrRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
+ gr_text(gr_sys_font(), x + stereo_offset_, y, line.c_str(), bold);
+ gr_text(gr_sys_font(), x - stereo_offset_ + ScreenWidth(), y, line.c_str(), bold);
return char_height_ + 4;
}
int VrRecoveryUI::DrawHorizontalRule(int y) const {
y += 4;
- gr_fill(kMarginWidth + kStereoOffset, y, ScreenWidth() - kMarginWidth + kStereoOffset, y + 2);
- gr_fill(ScreenWidth() + kMarginWidth - kStereoOffset, y,
- gr_fb_width() - kMarginWidth - kStereoOffset, y + 2);
+ gr_fill(margin_width_ + stereo_offset_, y, ScreenWidth() - margin_width_ + stereo_offset_, y + 2);
+ gr_fill(ScreenWidth() + margin_width_ - stereo_offset_, y,
+ gr_fb_width() - margin_width_ - stereo_offset_, y + 2);
return y + 4;
}
void VrRecoveryUI::DrawHighlightBar(int /* x */, int y, int /* width */, int height) const {
- gr_fill(kMarginWidth + kStereoOffset, y, ScreenWidth() - kMarginWidth + kStereoOffset, y + height);
- gr_fill(ScreenWidth() + kMarginWidth - kStereoOffset, y,
- gr_fb_width() - kMarginWidth - kStereoOffset, y + height);
+ gr_fill(margin_width_ + stereo_offset_, y, ScreenWidth() - margin_width_ + stereo_offset_,
+ y + height);
+ gr_fill(ScreenWidth() + margin_width_ - stereo_offset_, y,
+ gr_fb_width() - margin_width_ - stereo_offset_, y + height);
}
void VrRecoveryUI::DrawFill(int x, int y, int w, int h) const {
- gr_fill(x + kStereoOffset, y, w, h);
- gr_fill(x - kStereoOffset + ScreenWidth(), y, w, h);
+ gr_fill(x + stereo_offset_, y, w, h);
+ gr_fill(x - stereo_offset_ + ScreenWidth(), y, w, h);
}
diff --git a/vr_ui.h b/vr_ui.h
index eeb4589..63c0f24 100644
--- a/vr_ui.h
+++ b/vr_ui.h
@@ -17,6 +17,8 @@
#ifndef RECOVERY_VR_UI_H
#define RECOVERY_VR_UI_H
+#include <string>
+
#include "screen_ui.h"
class VrRecoveryUI : public ScreenRecoveryUI {
@@ -26,7 +28,7 @@
protected:
// Pixel offsets to move drawing functions to visible range.
// Can vary per device depending on screen size and lens distortion.
- const int kStereoOffset;
+ const int stereo_offset_;
int ScreenWidth() const override;
int ScreenHeight() const override;
@@ -36,7 +38,7 @@
void DrawHighlightBar(int x, int y, int width, int height) const override;
void DrawFill(int x, int y, int w, int h) const override;
void DrawTextIcon(int x, int y, GRSurface* surface) const override;
- int DrawTextLine(int x, int y, const char* line, bool bold) const override;
+ int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
};
#endif // RECOVERY_VR_UI_H
diff --git a/wear_ui.cpp b/wear_ui.cpp
index ca6b1b1..3b057b7 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -16,31 +16,31 @@
#include "wear_ui.h"
-#include <pthread.h>
-#include <stdio.h> // TODO: Remove after killing the call to sprintf().
#include <string.h>
#include <string>
+#include <vector>
#include <android-base/properties.h>
#include <android-base/strings.h>
#include <minui/minui.h>
-WearRecoveryUI::WearRecoveryUI()
- : kProgressBarBaseline(RECOVERY_UI_PROGRESS_BAR_BASELINE),
- kMenuUnusableRows(RECOVERY_UI_MENU_UNUSABLE_ROWS) {
- // TODO: kMenuUnusableRows should be computed based on the lines in draw_screen_locked().
+constexpr int kDefaultProgressBarBaseline = 259;
+constexpr int kDefaultMenuUnusableRows = 9;
- // TODO: The following three variables are likely not needed. The first two are detected
- // automatically in ScreenRecoveryUI::LoadAnimation(), based on the actual files seen on device.
- intro_frames = 22;
- loop_frames = 60;
+WearRecoveryUI::WearRecoveryUI()
+ : ScreenRecoveryUI(true),
+ progress_bar_baseline_(android::base::GetIntProperty("ro.recovery.ui.progress_bar_baseline",
+ kDefaultProgressBarBaseline)),
+ menu_unusable_rows_(android::base::GetIntProperty("ro.recovery.ui.menu_unusable_rows",
+ kDefaultMenuUnusableRows)) {
+ // TODO: menu_unusable_rows_ should be computed based on the lines in draw_screen_locked().
touch_screen_allowed_ = true;
}
int WearRecoveryUI::GetProgressBaseline() const {
- return kProgressBarBaseline;
+ return progress_bar_baseline_;
}
// Draw background frame on the screen. Does not flip pages.
@@ -58,20 +58,17 @@
int frame_x = (gr_fb_width() - frame_width) / 2;
int frame_y = (gr_fb_height() - frame_height) / 2;
gr_blit(frame, 0, 0, frame_width, frame_height, frame_x, frame_y);
+
+ // Draw recovery text on screen above progress bar.
+ GRSurface* text = GetCurrentText();
+ int text_x = (ScreenWidth() - gr_get_width(text)) / 2;
+ int text_y = GetProgressBaseline() - gr_get_height(text) - 10;
+ gr_color(255, 255, 255, 255);
+ gr_texticon(text_x, text_y, text);
}
}
-static const char* SWIPE_HELP[] = {
- "Swipe up/down to move.",
- "Swipe left/right to select.",
- "",
- NULL
-};
-
-// TODO merge drawing routines with screen_ui
void WearRecoveryUI::draw_screen_locked() {
- char cur_selection_str[50];
-
draw_background_locked();
if (!show_text) {
draw_foreground_locked();
@@ -79,68 +76,14 @@
SetColor(TEXT_FILL);
gr_fill(0, 0, gr_fb_width(), gr_fb_height());
- int y = kMarginHeight;
- int x = kMarginWidth;
- if (show_menu) {
- std::string recovery_fingerprint =
- android::base::GetProperty("ro.bootimage.build.fingerprint", "");
- SetColor(HEADER);
- y += DrawTextLine(x + 4, y, "Android Recovery", true);
- for (auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
- y += DrawTextLine(x + 4, y, chunk.c_str(), false);
- }
-
- // This is actually the help strings.
- y += DrawTextLines(x + 4, y, SWIPE_HELP);
- SetColor(HEADER);
- y += DrawTextLines(x + 4, y, menu_headers_);
-
- // Show the current menu item number in relation to total number if
- // items don't fit on the screen.
- if (menu_items > menu_end - menu_start) {
- sprintf(cur_selection_str, "Current item: %d/%d", menu_sel + 1, menu_items);
- gr_text(gr_sys_font(), x + 4, y, cur_selection_str, 1);
- y += char_height_ + 4;
- }
-
- // Menu begins here
- SetColor(MENU);
-
- for (int i = menu_start; i < menu_end; ++i) {
- if (i == menu_sel) {
- // draw the highlight bar
- SetColor(MENU_SEL_BG);
- gr_fill(x, y - 2, gr_fb_width() - x, y + char_height_ + 2);
- // white text of selected item
- SetColor(MENU_SEL_FG);
- if (menu_[i][0]) {
- gr_text(gr_sys_font(), x + 4, y, menu_[i].c_str(), 1);
- }
- SetColor(MENU);
- } else if (menu_[i][0]) {
- gr_text(gr_sys_font(), x + 4, y, menu_[i].c_str(), 0);
- }
- y += char_height_ + 4;
- }
- SetColor(MENU);
- y += 4;
- gr_fill(0, y, gr_fb_width(), y + 2);
- y += 4;
- }
-
- SetColor(LOG);
-
- // display from the bottom up, until we hit the top of the
- // screen, the bottom of the menu, or we've displayed the
- // entire text buffer.
- int row = text_row_;
- size_t count = 0;
- for (int ty = gr_fb_height() - char_height_ - kMarginHeight; ty > y + 2 && count < text_rows_;
- ty -= char_height_, ++count) {
- gr_text(gr_sys_font(), x + 4, ty, text_[row], 0);
- --row;
- if (row < 0) row = text_rows_ - 1;
- }
+ // clang-format off
+ static std::vector<std::string> SWIPE_HELP = {
+ "Swipe up/down to move.",
+ "Swipe left/right to select.",
+ "",
+ };
+ // clang-format on
+ draw_menu_and_text_buffer_locked(SWIPE_HELP);
}
}
@@ -152,49 +95,12 @@
void WearRecoveryUI::SetStage(int /* current */, int /* max */) {}
-void WearRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
- int initial_selection) {
- pthread_mutex_lock(&updateMutex);
+void WearRecoveryUI::StartMenu(const std::vector<std::string>& headers,
+ const std::vector<std::string>& items, size_t initial_selection) {
+ std::lock_guard<std::mutex> lg(updateMutex);
if (text_rows_ > 0 && text_cols_ > 0) {
- menu_headers_ = headers;
- menu_.clear();
- // "i < text_rows_" is removed from the loop termination condition,
- // which is different from the one in ScreenRecoveryUI::StartMenu().
- // Because WearRecoveryUI supports scrollable menu, it's fine to have
- // more entries than text_rows_. The menu may be truncated otherwise.
- // Bug: 23752519
- for (size_t i = 0; items[i] != nullptr; i++) {
- menu_.emplace_back(std::string(items[i], strnlen(items[i], text_cols_ - 1)));
- }
- menu_items = static_cast<int>(menu_.size());
- show_menu = true;
- menu_sel = initial_selection;
- menu_start = 0;
- menu_end = text_rows_ - 1 - kMenuUnusableRows;
- if (menu_items <= menu_end) menu_end = menu_items;
+ menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_ - menu_unusable_rows_ - 1,
+ text_cols_ - 1, headers, items, initial_selection);
update_screen_locked();
}
- pthread_mutex_unlock(&updateMutex);
-}
-
-int WearRecoveryUI::SelectMenu(int sel) {
- int old_sel;
- pthread_mutex_lock(&updateMutex);
- if (show_menu) {
- old_sel = menu_sel;
- menu_sel = sel;
- if (menu_sel < 0) menu_sel = 0;
- if (menu_sel >= menu_items) menu_sel = menu_items - 1;
- if (menu_sel < menu_start) {
- menu_start--;
- menu_end--;
- } else if (menu_sel >= menu_end && menu_sel < menu_items) {
- menu_end++;
- menu_start++;
- }
- sel = menu_sel;
- if (menu_sel != old_sel) update_screen_locked();
- }
- pthread_mutex_unlock(&updateMutex);
- return sel;
}
diff --git a/wear_ui.h b/wear_ui.h
index 739b4cb..b80cfd7 100644
--- a/wear_ui.h
+++ b/wear_ui.h
@@ -17,6 +17,9 @@
#ifndef RECOVERY_WEAR_UI_H
#define RECOVERY_WEAR_UI_H
+#include <string>
+#include <vector>
+
#include "screen_ui.h"
class WearRecoveryUI : public ScreenRecoveryUI {
@@ -25,18 +28,16 @@
void SetStage(int current, int max) override;
- // menu display
- void StartMenu(const char* const* headers, const char* const* items,
- int initial_selection) override;
- int SelectMenu(int sel) override;
-
protected:
// progress bar vertical position, it's centered horizontally
- const int kProgressBarBaseline;
+ const int progress_bar_baseline_;
// Unusable rows when displaying the recovery menu, including the lines for headers (Android
// Recovery, build id and etc) and the bottom lines that may otherwise go out of the screen.
- const int kMenuUnusableRows;
+ const int menu_unusable_rows_;
+
+ void StartMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
+ size_t initial_selection) override;
int GetProgressBaseline() const override;
@@ -45,8 +46,6 @@
private:
void draw_background_locked() override;
void draw_screen_locked() override;
-
- int menu_start, menu_end;
};
#endif // RECOVERY_WEAR_UI_H