diff options
Diffstat (limited to '')
32 files changed, 1170 insertions, 718 deletions
diff --git a/Android.mk b/Android.mk index fef5846ac..6aa91ea21 100644 --- a/Android.mk +++ b/Android.mk @@ -204,6 +204,5 @@ include \ $(LOCAL_PATH)/boot_control/Android.mk \ $(LOCAL_PATH)/minui/Android.mk \ $(LOCAL_PATH)/tests/Android.mk \ - $(LOCAL_PATH)/tools/Android.mk \ $(LOCAL_PATH)/updater/Android.mk \ $(LOCAL_PATH)/updater_sample/Android.mk \ diff --git a/applypatch/imgdiff.cpp b/applypatch/imgdiff.cpp index 674cc2b16..415d95f14 100644 --- a/applypatch/imgdiff.cpp +++ b/applypatch/imgdiff.cpp @@ -462,12 +462,12 @@ PatchChunk::PatchChunk(const ImageChunk& tgt) 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 c4c2707fb..2f8f4851d 100644 --- a/applypatch/imgpatch.cpp +++ b/applypatch/imgpatch.cpp @@ -54,6 +54,7 @@ static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_ const Value& patch, size_t patch_offset, const char* deflate_header, SinkFn sink) { size_t expected_target_length = static_cast<size_t>(Read8(deflate_header + 32)); + CHECK_GT(expected_target_length, 0); int level = Read4(deflate_header + 40); int method = Read4(deflate_header + 44); int window_bits = Read4(deflate_header + 48); diff --git a/applypatch/include/applypatch/imgdiff_image.h b/applypatch/include/applypatch/imgdiff_image.h index 084807237..671605160 100644 --- a/applypatch/include/applypatch/imgdiff_image.h +++ b/applypatch/include/applypatch/imgdiff_image.h @@ -44,6 +44,8 @@ class ImageChunk { int GetType() const { return type_; } + + const uint8_t* GetRawData() const; size_t GetRawDataLength() const { return raw_data_len_; } @@ -99,7 +101,6 @@ class ImageChunk { 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/bootloader_message/Android.bp b/bootloader_message/Android.bp index c81c67bdb..ab23733cd 100644 --- a/bootloader_message/Android.bp +++ b/bootloader_message/Android.bp @@ -16,6 +16,7 @@ cc_library_static { name: "libbootloader_message", + recovery_available: true, srcs: ["bootloader_message.cpp"], cflags: [ "-Wall", diff --git a/tests/Android.mk b/tests/Android.mk index 853ca273b..ff420668a 100644 --- a/tests/Android.mk +++ b/tests/Android.mk @@ -37,6 +37,7 @@ LOCAL_STATIC_LIBRARIES := \ LOCAL_SRC_FILES := \ unit/asn1_decoder_test.cpp \ + unit/commands_test.cpp \ unit/dirutil_test.cpp \ unit/locale_test.cpp \ unit/rangeset_test.cpp \ diff --git a/tests/component/imgdiff_test.cpp b/tests/component/imgdiff_test.cpp index 6c23def01..cb4868a4a 100644 --- a/tests/component/imgdiff_test.cpp +++ b/tests/component/imgdiff_test.cpp @@ -197,12 +197,17 @@ TEST(ImgdiffTest, zip_mode_smoke_store) { } 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 @@ TEST(ImgdiffTest, zip_mode_smoke_compressed) { 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 @@ TEST(ImgdiffTest, zip_mode_smoke_compressed) { 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 @@ TEST(ImgdiffTest, zip_mode_smoke_trailer_zeros) { 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, zip_mode_smoke_trailer_zeros) { } 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_different_num_chunks) { } 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/updater_test.cpp b/tests/component/updater_test.cpp index 48363a62b..de8fafd30 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> @@ -48,7 +50,11 @@ #include "updater/install.h" #include "updater/updater.h" -struct selabel_handle *sehandle = nullptr; +using PackageEntries = std::unordered_map<std::string, std::string>; + +static constexpr size_t kTransferListHeaderLines = 4; + +struct selabel_handle* sehandle = nullptr; static void expect(const char* expected, const char* expr_str, CauseCode cause_code, UpdaterInfo* info = nullptr) { @@ -76,12 +82,12 @@ static void expect(const char* expected, const char* expr_str, CauseCode cause_c 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,6 +99,37 @@ static void BuildUpdatePackage(const std::unordered_map<std::string, std::string 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) { + 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.c_str(), kNoCause, &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); @@ -101,19 +138,39 @@ static std::string get_sha1(const std::string& content) { class UpdaterTest : public ::testing::Test { protected: - virtual void SetUp() override { + void SetUp() override { RegisterBuiltins(); RegisterInstallFunctions(); RegisterBlockImageFunctions(); + // 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); + + 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) { @@ -453,16 +510,18 @@ TEST_F(UpdaterTest, block_image_update_patch_data) { // 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", @@ -471,183 +530,108 @@ TEST_F(UpdaterTest, block_image_update_patch_data) { 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, + "fail", + // 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", + std::vector<std::string> transfer_list{ + // clang-format off + "4", + "1", + "0", + "0", + "new 2,0,1", + // clang-format on }; // Write 4096 + 100 bytes of new data. - std::unordered_map<std::string, std::string> entries = { + PackageEntries 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); + RunBlockImageUpdate(false, entries, image_file_, "t"); } 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') }, + PackageEntries entries{ { "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(), "wbe"); - updater_info.package_zip_addr = map.addr; - updater_info.package_zip_len = map.length; - // 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) { @@ -680,55 +664,30 @@ TEST_F(UpdaterTest, brotli_new_data) { "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 = Paths::Get().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,10 +696,12 @@ TEST_F(UpdaterTest, last_command_update) { "move " + block1_hash + " 2,1,2 1 2,0,1", "stash " + block3_hash + " 2,2,3", "fail", + // 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", @@ -749,127 +710,86 @@ TEST_F(UpdaterTest, last_command_update) { "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[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 = Paths::Get().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[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; - - // 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)); + RunBlockImageUpdate(false, entries, image_file_, ""); - 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 = Paths::Get().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", @@ -878,55 +798,33 @@ TEST_F(UpdaterTest, last_command_verify) { "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()); - - 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; + ASSERT_TRUE(android::base::WriteStringToFile(block1 + block1 + block3, image_file_)); - std::string src_content = block1 + block1 + block3; - TemporaryFile update_file; - ASSERT_TRUE(android::base::WriteStringToFile(src_content, update_file.path)); + // Last command: "move " + block1_hash + " 2,1,2 1 2,0,1" + std::string last_command_content = "2\n" + transfer_list_verify[kTransferListHeaderLines + 2]; - ASSERT_TRUE( - android::base::WriteStringToFile("2\nstash " + block3_hash + " 2,2,3", last_command_file)); + // 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_)); - // 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); + RunBlockImageUpdate(true, entries, image_file_, "t"); - 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); - // 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)); - - ASSERT_EQ(0, fclose(updater_info.cmd_pipe)); - CloseArchive(handle); + // 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)); } diff --git a/tests/testdata/gzipped_source b/tests/testdata/gzipped_source Binary files differnew file mode 100644 index 000000000..6d425d059 --- /dev/null +++ b/tests/testdata/gzipped_source diff --git a/tests/testdata/gzipped_target b/tests/testdata/gzipped_target Binary files differnew file mode 100644 index 000000000..562126286 --- /dev/null +++ b/tests/testdata/gzipped_target diff --git a/tests/unit/commands_test.cpp b/tests/unit/commands_test.cpp new file mode 100644 index 000000000..18aa471ab --- /dev/null +++ b/tests/unit/commands_test.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <string> + +#include <gtest/gtest.h> + +#include "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")); +} + +TEST(CommandsTest, ParseType_InvalidCommand) { + ASSERT_EQ(Command::Type::LAST, Command::ParseType("foo")); + ASSERT_EQ(Command::Type::LAST, Command::ParseType("bar")); +} diff --git a/tools/Android.mk b/tools/Android.mk deleted file mode 100644 index 65711611c..000000000 --- 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 000000000..eb45e3176 --- /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/dumpkey/Android.mk b/tools/dumpkey/Android.mk deleted file mode 100644 index 31549146d..000000000 --- a/tools/dumpkey/Android.mk +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2008 The Android Open Source Project -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -LOCAL_PATH := $(call my-dir) - -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) diff --git a/tools/recovery_l10n/Android.bp b/tools/recovery_l10n/Android.bp new file mode 100644 index 000000000..d0a6d4b47 --- /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 7197c5c78..000000000 --- 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/updater/Android.mk b/updater/Android.mk index 476266400..46c56f4a0 100644 --- a/updater/Android.mk +++ b/updater/Android.mk @@ -56,6 +56,7 @@ include $(CLEAR_VARS) LOCAL_MODULE := libupdater LOCAL_SRC_FILES := \ + commands.cpp \ install.cpp \ blockimg.cpp diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp index 236644e7f..5d6da6cb3 100644 --- a/updater/blockimg.cpp +++ b/updater/blockimg.cpp @@ -57,6 +57,7 @@ #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" @@ -82,7 +83,7 @@ static void DeleteLastCommandFile() { // 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) { +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) { @@ -133,7 +134,7 @@ static bool FsyncDir(const std::string& dirname) { } // Update the last executed command index in the last_command_file. -static bool UpdateLastCommandIndex(int command_index, const std::string& command_string) { +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; @@ -546,9 +547,8 @@ static int WriteBlocks(const RangeSet& tgt, const std::vector<uint8_t>& buffer, 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; @@ -1497,23 +1497,13 @@ static int PerformCommandErase(CommandParameters& params) { return 0; } -// Definitions for transfer list command functions -typedef int (*CommandFunction)(CommandParameters&); +using CommandFunction = std::function<int(CommandParameters&)>; -struct Command { - const char* name; - CommandFunction f; -}; - -// 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) +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 = {}; params.canwrite = !dryrun; @@ -1533,6 +1523,11 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, 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]; @@ -1666,7 +1661,6 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, return StringValue("t"); } - size_t start = 2; if (lines.size() < 4) { ErrorAbort(state, kArgsParsingFailure, "too few lines in the transfer list [%zu]", lines.size()); @@ -1691,8 +1685,8 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, params.createdstash = res; - // When performing an update, save the index and cmdline of the current command into - // the last_command_file. + // 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 @@ -1701,87 +1695,73 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, // 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(""); - } - 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; + static constexpr size_t kTransferListHeaderLines = 4; // 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; } - std::string cmdname = std::string(params.cmdname); - // 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 && params.cmdindex != -1 && params.cmdindex <= saved_last_command_index && - cmdname != "new") { - LOG(INFO) << "Skipping already executed command: " << params.cmdindex + 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 << "]"; 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. - 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; @@ -1789,7 +1769,7 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, goto pbiudone; } - if (!UpdateLastCommandIndex(params.cmdindex, params.cmdline)) { + if (!UpdateLastCommandIndex(cmdindex, params.cmdline)) { LOG(WARNING) << "Failed to update the last command file."; } @@ -1918,38 +1898,42 @@ pbiudone: */ 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 } - }; - - // 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); + // Commands which are not allowed are set to nullptr to skip them completely. + const CommandMap command_map{ + // clang-format off + { Command::Type::BSDIFF, PerformCommandDiff }, + { 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, 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 } - }; - - return PerformBlockImageUpdate(name, state, argv, commands, - sizeof(commands) / sizeof(commands[0]), false); + const CommandMap command_map{ + // clang-format off + { Command::Type::BSDIFF, PerformCommandDiff }, + { 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, command_map, false); } Value* RangeSha1Fn(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv) { diff --git a/updater/commands.cpp b/updater/commands.cpp new file mode 100644 index 000000000..f798c6a73 --- /dev/null +++ b/updater/commands.cpp @@ -0,0 +1,43 @@ +/* + * 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 <string> + +#include <android-base/logging.h> + +Command::Type Command::ParseType(const std::string& type_str) { + if (type_str == "zero") { + return Type::ZERO; + } else if (type_str == "new") { + return Type::NEW; + } else if (type_str == "erase") { + return Type::ERASE; + } else if (type_str == "move") { + return Type::MOVE; + } else if (type_str == "bsdiff") { + return Type::BSDIFF; + } else if (type_str == "imgdiff") { + return Type::IMGDIFF; + } else if (type_str == "stash") { + return Type::STASH; + } else if (type_str == "free") { + return Type::FREE; + } + LOG(ERROR) << "Invalid type: " << type_str; + return Type::LAST; +}; diff --git a/updater/include/private/commands.h b/updater/include/private/commands.h new file mode 100644 index 000000000..b36000072 --- /dev/null +++ b/updater/include/private/commands.h @@ -0,0 +1,35 @@ +/* + * 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 <string> + +struct Command { + enum class Type { + ZERO, + NEW, + ERASE, + MOVE, + BSDIFF, + IMGDIFF, + STASH, + FREE, + LAST, // Not a valid type. + }; + + static Type ParseType(const std::string& type_str); +}; diff --git a/updater_sample/OWNERS b/updater_sample/OWNERS new file mode 100644 index 000000000..5c1c3706c --- /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 index c68c07caf..3f211ddba 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -90,9 +90,9 @@ which HTTP headers are supported. - [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 -- [ ] Add tests for `MainActivity` +- [ ] Add demo for passing NETWORK_ID to `UpdateEngine#applyPayload` - [ ] Verify system partition checksum for package -- [ ] Add non-A/B updates demo +- [?] Add non-A/B updates demo ## Running tests diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml index d9e56b4b3..7cde42cec 100644 --- a/updater_sample/res/layout/activity_main.xml +++ b/updater_sample/res/layout/activity_main.xml @@ -111,19 +111,38 @@ android:orientation="horizontal"> <TextView - android:id="@+id/textView" + android:id="@+id/textView3" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Update status:" /> + android:text="Updater state:" /> <TextView - android:id="@+id/textViewStatus" + 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" @@ -135,10 +154,10 @@ android:id="@+id/textView2" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="Update completion:" /> + android:text="Engine error:" /> <TextView - android:id="@+id/textViewCompletion" + android:id="@+id/textViewEngineErrorCode" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="8dp" diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java index db99f7c74..1e0fadc27 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java @@ -279,4 +279,4 @@ public class UpdateConfig implements Parcelable { } -}
\ No newline at end of file +} 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 000000000..c370a4eb5 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.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.example.android.systemupdatersample.util.UpdaterStates; +import com.google.common.util.concurrent.AtomicDouble; + +import java.io.IOException; +import java.util.ArrayList; +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; + +/** + * 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 AtomicInteger mState = new AtomicInteger(UpdaterStates.IDLE); + + private final UpdateManager.UpdateEngineCallbackImpl + mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); + + private PayloadSpec mLastPayloadSpec; + private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); + + private IntConsumer mOnStateChangeCallback = null; + private IntConsumer mOnEngineStatusUpdateCallback = null; + private DoubleConsumer mOnProgressUpdateCallback = null; + private IntConsumer mOnEngineCompleteCallback = null; + + private final Object mLock = new Object(); + + public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) { + this.mUpdateEngine = updateEngine; + this.mPayloadSpecs = payloadSpecs; + } + + /** + * Binds to {@link UpdateEngine}. + */ + public void bind() { + this.mUpdateEngine.bind(mUpdateEngineCallback); + } + + /** + * Unbinds from {@link UpdateEngine}. + */ + public void unbind() { + this.mUpdateEngine.unbind(); + } + + /** + * @return a number from {@code 0.0} to {@code 1.0}. + */ + public float getProgress() { + return (float) this.mProgress.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 UpdaterStates}. + * + * @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); + } + } + + /** + * Updates {@link this.mState} and if state is changed, + * it also notifies {@link this.mOnStateChangeCallback}. + */ + private void setUpdaterState(int updaterState) { + int previousState = mState.get(); + mState.set(updaterState); + if (previousState != updaterState) { + getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState)); + } + } + + /** + * Requests update engine to stop any ongoing update. If an update has been applied, + * leave it as is. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void cancelRunningUpdate() { + try { + mUpdateEngine.cancel(); + setUpdaterState(UpdaterStates.IDLE); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); + } + } + + /** + * Resets update engine to IDLE state. If an update has been applied it reverts it. + * + * <p>Sometimes it's possible that the + * update engine would throw an error when the method is called, and the only way to + * handle it is to catch the exception.</p> + */ + public void resetUpdate() { + try { + mUpdateEngine.resetStatus(); + setUpdaterState(UpdaterStates.IDLE); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine failed to reset the update", e); + } + } + + /** + * Applies the given update. + * + * <p>UpdateEngine works asynchronously. This method doesn't wait until + * end of the update.</p> + */ + public void applyUpdate(Context context, UpdateConfig config) { + mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN); + setUpdaterState(UpdaterStates.RUNNING); + + 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) { + List<String> extraProperties = prepareExtraProperties(config); + + PayloadSpec payload; + try { + payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } catch (IOException e) { + Log.e(TAG, "Error creating payload spec", e); + setUpdaterState(UpdaterStates.ERROR); + return; + } + updateEngineApplyPayload(payload, extraProperties); + } + + private void applyAbStreamingUpdate(Context context, UpdateConfig config) { + List<String> extraProperties = prepareExtraProperties(config); + + Log.d(TAG, "Starting PrepareStreamingService"); + PrepareStreamingService.startService(context, config, (code, payloadSpec) -> { + if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { + extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); + config.getStreamingMetadata() + .getAuthorization() + .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); + updateEngineApplyPayload(payloadSpec, extraProperties); + } else { + Log.e(TAG, "PrepareStreamingService failed, result code is " + code); + setUpdaterState(UpdaterStates.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> + * + * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME} + * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload} + */ + private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) { + mLastPayloadSpec = payloadSpec; + + ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties()); + if (extraProperties != null) { + properties.addAll(extraProperties); + } + try { + mUpdateEngine.applyPayload( + payloadSpec.getUrl(), + payloadSpec.getOffset(), + payloadSpec.getSize(), + properties.toArray(new String[0])); + } catch (Exception e) { + Log.e(TAG, "UpdateEngine failed to apply the update", e); + setUpdaterState(UpdaterStates.ERROR); + } + } + + /** + * 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 void setSwitchSlotOnReboot() { + Log.d(TAG, "setSwitchSlotOnReboot invoked"); + List<String> extraProperties = new ArrayList<>(); + // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks. + extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL); + // It sets property SWITCH_SLOT_ON_REBOOT=1 by default. + // HTTP headers are not required, UpdateEngine is not expected to stream payload. + updateEngineApplyPayload(mLastPayloadSpec, extraProperties); + } + + private void onStatusUpdate(int status, float progress) { + int previousStatus = mUpdateEngineStatus.get(); + mUpdateEngineStatus.set(status); + mProgress.set(progress); + + getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress)); + + 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) { + setUpdaterState(UpdaterStates.FINISHED); + } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) { + setUpdaterState(UpdaterStates.ERROR); + } + + getOnEngineCompleteCallback() + .ifPresent(callback -> callback.accept(errorCode)); + } + + /** + * Helper class to delegate {@code update_engine} callbacks 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); + } + } + +} diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java index 9bab1319d..9983fe316 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -22,7 +22,6 @@ import android.graphics.Color; import android.os.Build; import android.os.Bundle; import android.os.UpdateEngine; -import android.os.UpdateEngineCallback; import android.util.Log; import android.view.View; import android.widget.ArrayAdapter; @@ -30,23 +29,17 @@ import android.widget.Button; import android.widget.ProgressBar; import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; -import com.example.android.systemupdatersample.PayloadSpec; import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.UpdateConfig; -import com.example.android.systemupdatersample.services.PrepareStreamingService; +import com.example.android.systemupdatersample.UpdateManager; 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.UpdateEngineProperties; import com.example.android.systemupdatersample.util.UpdateEngineStatuses; +import com.example.android.systemupdatersample.util.UpdaterStates; -import java.io.IOException; -import java.util.ArrayList; import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; /** * UI for SystemUpdaterSample app. @@ -55,10 +48,6 @@ public class MainActivity extends Activity { private static final String TAG = "MainActivity"; - /** 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 TextView mTextViewBuild; private Spinner mSpinnerConfigs; private TextView mTextViewConfigsDirHint; @@ -67,24 +56,16 @@ public class MainActivity extends Activity { private Button mButtonStop; private Button mButtonReset; private ProgressBar mProgressBar; - private TextView mTextViewStatus; - private TextView mTextViewCompletion; + private TextView mTextViewUpdaterState; + private TextView mTextViewEngineStatus; + private TextView mTextViewEngineErrorCode; private TextView mTextViewUpdateInfo; private Button mButtonSwitchSlot; private List<UpdateConfig> mConfigs; - private AtomicInteger mUpdateEngineStatus = - new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); - private PayloadSpec mLastPayloadSpec; - private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true); - private final PayloadSpecs mPayloadSpecs = new PayloadSpecs(); - - /** - * Listen to {@code update_engine} events. - */ - private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl(); - private final UpdateEngine mUpdateEngine = new UpdateEngine(); + private final UpdateManager mUpdateManager = + new UpdateManager(new UpdateEngine(), new PayloadSpecs()); @Override protected void onCreate(Bundle savedInstanceState) { @@ -99,8 +80,9 @@ public class MainActivity extends Activity { this.mButtonStop = findViewById(R.id.buttonStop); this.mButtonReset = findViewById(R.id.buttonReset); this.mProgressBar = findViewById(R.id.progressBar); - this.mTextViewStatus = findViewById(R.id.textViewStatus); - this.mTextViewCompletion = findViewById(R.id.textViewCompletion); + 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); @@ -109,15 +91,32 @@ public class MainActivity extends Activity { uiReset(); loadUpdateConfigs(); - this.mUpdateEngine.bind(mUpdateEngineCallback); + 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.mUpdateEngine.unbind(); + this.mUpdateManager.setOnEngineStatusUpdateCallback(null); + this.mUpdateManager.setOnProgressUpdateCallback(null); + this.mUpdateManager.setOnEngineCompleteCallback(null); super.onDestroy(); } + @Override + protected void onResume() { + super.onResume(); + this.mUpdateManager.bind(); + } + + @Override + protected void onPause() { + this.mUpdateManager.unbind(); + super.onPause(); + } + /** * reload button is clicked */ @@ -147,7 +146,8 @@ public class MainActivity extends Activity { .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { uiSetUpdating(); - applyUpdate(getSelectedConfig()); + uiResetEngineText(); + mUpdateManager.applyUpdate(this, getSelectedConfig()); }) .setNegativeButton(android.R.string.cancel, null) .show(); @@ -162,7 +162,7 @@ public class MainActivity extends Activity { .setMessage("Do you really want to cancel running update?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - stopRunningUpdate(); + mUpdateManager.cancelRunningUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); } @@ -177,7 +177,7 @@ public class MainActivity extends Activity { + " and restore old version?") .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> { - resetUpdate(); + mUpdateManager.resetUpdate(); }) .setNegativeButton(android.R.string.cancel, null).show(); } @@ -186,34 +186,39 @@ public class MainActivity extends Activity { * switch slot button clicked */ public void onSwitchSlotClick(View view) { - setSwitchSlotOnReboot(); + mUpdateManager.setSwitchSlotOnReboot(); } /** - * Invoked when anything changes. The value of {@code status} will - * be one of the values from {@link UpdateEngine.UpdateStatusConstants}, - * and {@code percent} will be from {@code 0.0} to {@code 1.0}. + * Invoked when SystemUpdaterSample app state changes. + * Value of {@code state} will be one of the + * values from {@link UpdaterStates}. */ - private void onStatusUpdate(int status, float percent) { - mProgressBar.setProgress((int) (100 * percent)); - if (mUpdateEngineStatus.get() != status) { - mUpdateEngineStatus.set(status); - runOnUiThread(() -> { - Log.e("UpdateEngine", "StatusUpdate - status=" - + UpdateEngineStatuses.getStatusText(status) - + "/" + status); - Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG) - .show(); - if (status == UpdateEngine.UpdateStatusConstants.IDLE) { - Log.d(TAG, "status changed, resetting ui"); - uiReset(); - } else { - Log.d(TAG, "status changed, setting ui to updating mode"); - uiSetUpdating(); - } - setUiStatus(status); - }); - } + private void onUpdaterStateChange(int state) { + Log.i(TAG, "onUpdaterStateChange invoked state=" + state); + runOnUiThread(() -> { + setUiUpdaterState(state); + }); + } + + /** + * 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) { + runOnUiThread(() -> { + Log.e(TAG, "StatusUpdate - status=" + + UpdateEngineStatuses.getStatusText(status) + + "/" + status); + if (status == UpdateEngine.UpdateStatusConstants.IDLE) { + Log.d(TAG, "status changed, resetting ui"); + uiReset(); + } else { + Log.d(TAG, "status changed, setting ui to updating mode"); + uiSetUpdating(); + } + setUiEngineStatus(status); + }); } /** @@ -221,20 +226,19 @@ public class MainActivity extends Activity { * unsuccessfully. The value of {@code errorCode} will be one of the * values from {@link UpdateEngine.ErrorCodeConstants}. */ - private void onPayloadApplicationComplete(int errorCode) { - final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) + private void onEnginePayloadApplicationComplete(int errorCode) { + final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) ? "SUCCESS" : "FAILURE"; runOnUiThread(() -> { - Log.i("UpdateEngine", + Log.i(TAG, "Completed - errorCode=" + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode - + " " + state); - Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show(); - setUiCompletion(errorCode); + + " " + completionState); + setUiEngineErrorCode(errorCode); if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) { // if update was successfully applied. - if (mManualSwitchSlotRequired.get()) { + if (mUpdateManager.isManualSwitchSlotRequired()) { // Show "Switch Slot" button. uiShowSwitchSlotInfo(); } @@ -242,6 +246,13 @@ public class MainActivity extends Activity { }); } + /** + * Invoked when update progress changes. + */ + private void onProgressUpdate(double progress) { + mProgressBar.setProgress((int) (100 * progress)); + } + /** resets ui */ private void uiReset() { mTextViewBuild.setText(Build.DISPLAY); @@ -253,11 +264,15 @@ public class MainActivity extends Activity { mProgressBar.setProgress(0); mProgressBar.setEnabled(false); mProgressBar.setVisibility(ProgressBar.INVISIBLE); - mTextViewStatus.setText(R.string.unknown); - mTextViewCompletion.setText(R.string.unknown); uiHideSwitchSlotInfo(); } + private void uiResetEngineText() { + mTextViewEngineStatus.setText(R.string.unknown); + mTextViewEngineErrorCode.setText(R.string.unknown); + // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies properly. + } + /** sets ui updating mode */ private void uiSetUpdating() { mTextViewBuild.setText(Build.DISPLAY); @@ -291,20 +306,25 @@ public class MainActivity extends Activity { /** * @param status update engine status code */ - private void setUiStatus(int status) { + private void setUiEngineStatus(int status) { String statusText = UpdateEngineStatuses.getStatusText(status); - mTextViewStatus.setText(statusText + "/" + status); + mTextViewEngineStatus.setText(statusText + "/" + status); } /** * @param errorCode update engine error code */ - private void setUiCompletion(int errorCode) { - final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode) - ? "SUCCESS" - : "FAILURE"; + private void setUiEngineErrorCode(int errorCode) { String errorText = UpdateEngineErrorCodes.getCodeName(errorCode); - mTextViewCompletion.setText(state + " " + errorText + "/" + errorCode); + mTextViewEngineErrorCode.setText(errorText + "/" + errorCode); + } + + /** + * @param state updater sample state + */ + private void setUiUpdaterState(int state) { + String stateText = UpdaterStates.getStateText(state); + mTextViewUpdaterState.setText(stateText + "/" + state); } private void loadConfigsToSpinner(List<UpdateConfig> configs) { @@ -321,143 +341,4 @@ public class MainActivity extends Activity { return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition()); } - /** - * Applies the given update - */ - private void applyUpdate(final 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); - mManualSwitchSlotRequired.set(true); - } else { - mManualSwitchSlotRequired.set(false); - } - - if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { - PayloadSpec payload; - try { - payload = mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); - } catch (IOException e) { - Log.e(TAG, "Error creating payload spec", e); - Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG) - .show(); - return; - } - updateEngineApplyPayload(payload, extraProperties); - } else { - Log.d(TAG, "Starting PrepareStreamingService"); - PrepareStreamingService.startService(this, config, (code, payloadSpec) -> { - if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { - extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); - config.getStreamingMetadata() - .getAuthorization() - .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); - updateEngineApplyPayload(payloadSpec, extraProperties); - } else { - Log.e(TAG, "PrepareStreamingService failed, result code is " + code); - Toast.makeText( - MainActivity.this, - "PrepareStreamingService failed, result code is " + code, - Toast.LENGTH_LONG).show(); - } - }); - } - } - - /** - * Applies given payload. - * - * UpdateEngine works asynchronously. This method doesn't wait until - * end of the update. - * - * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME} - * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload} - */ - private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) { - mLastPayloadSpec = payloadSpec; - - ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties()); - if (extraProperties != null) { - properties.addAll(extraProperties); - } - try { - mUpdateEngine.applyPayload( - payloadSpec.getUrl(), - payloadSpec.getOffset(), - payloadSpec.getSize(), - properties.toArray(new String[0])); - } catch (Exception e) { - Log.e(TAG, "UpdateEngine failed to apply the update", e); - Toast.makeText( - this, - "UpdateEngine failed to apply the update", - Toast.LENGTH_LONG).show(); - } - } - - /** - * 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)}. - */ - private void setSwitchSlotOnReboot() { - Log.d(TAG, "setSwitchSlotOnReboot invoked"); - List<String> extraProperties = new ArrayList<>(); - // PROPERTY_SKIP_POST_INSTALL should be passed on to skip post-installation hooks. - extraProperties.add(UpdateEngineProperties.PROPERTY_SKIP_POST_INSTALL); - // It sets property SWITCH_SLOT_ON_REBOOT=1 by default. - // HTTP headers are not required, UpdateEngine is not expected to stream payload. - updateEngineApplyPayload(mLastPayloadSpec, extraProperties); - uiHideSwitchSlotInfo(); - } - - /** - * Requests update engine to stop any ongoing update. If an update has been applied, - * leave it as is. - */ - private void stopRunningUpdate() { - try { - mUpdateEngine.cancel(); - } catch (Exception e) { - Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e); - } - } - - /** - * Resets update engine to IDLE state. Requests to cancel any onging update, or to revert if an - * update has been applied. - */ - private void resetUpdate() { - try { - mUpdateEngine.resetStatus(); - } catch (Exception e) { - Log.w(TAG, "UpdateEngine failed to reset the update", e); - } - } - - /** - * Helper class to delegate {@code update_engine} callbacks to MainActivity - */ - class UpdateEngineCallbackImpl extends UpdateEngineCallback { - @Override - public void onStatusUpdate(int status, float percent) { - MainActivity.this.onStatusUpdate(status, percent); - } - - @Override - public void onPayloadApplicationComplete(int errorCode) { - MainActivity.this.onPayloadApplicationComplete(errorCode); - } - } - } diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java index b98b97c37..f06231726 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java @@ -32,7 +32,9 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; /** The helper class that creates {@link PayloadSpec}. */ -public final class PayloadSpecs { +public class PayloadSpecs { + + public PayloadSpecs() {} /** * The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java index 6d319c5af..7d55ff8fc 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java @@ -34,7 +34,9 @@ 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<>(); @@ -60,7 +62,7 @@ public final class UpdateEngineErrorCodes { * Completion codes returned by update engine indicating that the update * was successfully applied. */ - private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<Integer>( + 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 diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java new file mode 100644 index 000000000..fc20a7941 --- /dev/null +++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdaterStates.java @@ -0,0 +1,50 @@ +/* + * 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; + +/** + * SystemUpdaterSample app state. + */ +public class UpdaterStates { + + 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 FINISHED = 4; + + 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, "FINISHED"); + } + + /** + * converts status code to status name + */ + public static String getStateText(int state) { + return STATE_MAP.get(state); + } + + private UpdaterStates() {} +} diff --git a/updater_sample/tests/Android.mk b/updater_sample/tests/Android.mk index a1a4664dc..9aec372e3 100644 --- a/updater_sample/tests/Android.mk +++ b/updater_sample/tests/Android.mk @@ -23,9 +23,9 @@ LOCAL_MODULE_TAGS := tests LOCAL_JAVA_LIBRARIES := \ android.test.base.stubs \ android.test.runner.stubs \ - guava \ + guava +LOCAL_STATIC_JAVA_LIBRARIES := android-support-test \ mockito-target-minus-junit4 -LOCAL_STATIC_JAVA_LIBRARIES := android-support-test LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample LOCAL_PROGUARD_ENABLED := disabled 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 000000000..0657a5eb6 --- /dev/null +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java @@ -0,0 +1,92 @@ +/* + * 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.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.os.UpdateEngine; +import android.os.UpdateEngineCallback; +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; + +import com.example.android.systemupdatersample.util.PayloadSpecs; + +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.util.function.IntConsumer; + +/** + * 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 mUpdateManager; + + @Before + public void setUp() { + mUpdateManager = new UpdateManager(mUpdateEngine, mPayloadSpecs); + } + + @Test + public void storesProgressThenInvokesCallbacks() { + IntConsumer statusUpdateCallback = mock(IntConsumer.class); + + // When UpdateManager is bound to update_engine, it passes + // UpdateManager.UpdateEngineCallbackImpl as a callback to update_engine. + when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { + UpdateEngineCallback callback = answer.getArgument(0); + callback.onStatusUpdate(/*engineStatus*/ 4, /*engineProgress*/ 0.2f); + return null; + }); + + mUpdateManager.setOnEngineStatusUpdateCallback(statusUpdateCallback); + + // Making sure that manager.getProgress() returns correct progress + // in "onEngineStatusUpdate" callback. + doAnswer(answer -> { + assertEquals(0.2f, mUpdateManager.getProgress(), 1E-5); + return null; + }).when(statusUpdateCallback).accept(anyInt()); + + mUpdateManager.bind(); + + verify(statusUpdateCallback, times(1)).accept(4); + } + +} diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java deleted file mode 100644 index 01014168a..000000000 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/ui/MainActivityTest.java +++ /dev/null @@ -1,48 +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. - */ - -package com.example.android.systemupdatersample.ui; - -import static org.junit.Assert.assertNotNull; - -import android.support.test.filters.MediumTest; -import android.support.test.rule.ActivityTestRule; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Make sure that the main launcher activity opens up properly, which will be - * verified by {@link #activityLaunches}. - */ -@RunWith(AndroidJUnit4.class) -@MediumTest -public class MainActivityTest { - - @Rule - public final ActivityTestRule<MainActivity> mActivityRule = - new ActivityTestRule<>(MainActivity.class); - - /** - * Verifies that the activity under test can be launched. - */ - @Test - public void activityLaunches() { - assertNotNull(mActivityRule.getActivity()); - } -} |