diff options
Diffstat (limited to '')
16 files changed, 758 insertions, 201 deletions
diff --git a/updater/Android.bp b/updater/Android.bp index c95ec5e90..b80cdb3a0 100644 --- a/updater/Android.bp +++ b/updater/Android.bp @@ -26,12 +26,14 @@ cc_defaults { "libedify", "libotautil", "libext4_utils", + "libdm", "libfec", "libfec_rs", "libverity_tree", "libfs_mgr", "libgtest_prod", "liblog", + "liblp", "libselinux", "libsparse", "libsquashfs_utils", @@ -66,6 +68,7 @@ cc_library_static { srcs: [ "blockimg.cpp", "commands.cpp", + "dynamic_partitions.cpp", "install.cpp", ], diff --git a/updater/Android.mk b/updater/Android.mk index 78e32ba39..c7a6ba989 100644 --- a/updater/Android.mk +++ b/updater/Android.mk @@ -29,12 +29,14 @@ updater_common_static_libraries := \ libedify \ libotautil \ libext4_utils \ + libdm \ libfec \ libfec_rs \ libverity_tree \ libfs_mgr \ libgtest_prod \ liblog \ + liblp \ libselinux \ libsparse \ libsquashfs_utils \ diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp index 838845673..07c3c7b52 100644 --- a/updater/blockimg.cpp +++ b/updater/blockimg.cpp @@ -53,6 +53,7 @@ #include <ziparchive/zip_archive.h> #include "edify/expr.h" +#include "otautil/dirutil.h" #include "otautil/error_code.h" #include "otautil/paths.h" #include "otautil/print_sha1.h" @@ -69,6 +70,7 @@ static constexpr size_t BLOCKSIZE = 4096; static constexpr mode_t STASH_DIRECTORY_MODE = 0700; static constexpr mode_t STASH_FILE_MODE = 0600; +static constexpr mode_t MARKER_DIRECTORY_MODE = 0700; static CauseCode failure_type = kNoCause; static bool is_retry = false; @@ -109,7 +111,7 @@ static bool ParseLastCommandFile(size_t* last_command_index) { return false; } - if (!android::base::ParseInt(lines[0], last_command_index)) { + if (!android::base::ParseUint(lines[0], last_command_index)) { LOG(ERROR) << "Failed to parse integer in: " << lines[0]; return false; } @@ -166,26 +168,37 @@ static bool UpdateLastCommandIndex(size_t command_index, const std::string& comm return true; } -static bool SetPartitionUpdatedMarker(const std::string& marker) { +bool SetUpdatedMarker(const std::string& marker) { + auto dirname = android::base::Dirname(marker); + auto res = mkdir(dirname.c_str(), MARKER_DIRECTORY_MODE); + if (res == -1 && errno != EEXIST) { + PLOG(ERROR) << "Failed to create directory for marker: " << dirname; + return false; + } + if (!android::base::WriteStringToFile("", marker)) { PLOG(ERROR) << "Failed to write to marker file " << marker; return false; } - if (!FsyncDir(android::base::Dirname(marker))) { + if (!FsyncDir(dirname)) { return false; } - LOG(INFO) << "Wrote partition updated marker to " << marker; + LOG(INFO) << "Wrote updated marker to " << marker; return true; } -static bool discard_blocks(int fd, off64_t offset, uint64_t size) { - // Don't discard blocks unless the update is a retry run. - if (!is_retry) { +static bool discard_blocks(int fd, off64_t offset, uint64_t size, bool force = false) { + // Don't discard blocks unless the update is a retry run or force == true + if (!is_retry && !force) { return true; } uint64_t args[2] = { static_cast<uint64_t>(offset), size }; if (ioctl(fd, BLKDISCARD, &args) == -1) { + // On devices that does not support BLKDISCARD, ignore the error. + if (errno == EOPNOTSUPP) { + return true; + } PLOG(ERROR) << "BLKDISCARD ioctl failed"; return false; } @@ -874,7 +887,7 @@ static int CreateStash(State* state, size_t maxblocks, const std::string& base) size_t max_stash_size = maxblocks * BLOCKSIZE; if (res == -1) { LOG(INFO) << "creating stash " << dirname; - res = mkdir(dirname.c_str(), STASH_DIRECTORY_MODE); + res = mkdir_recursively(dirname, STASH_DIRECTORY_MODE, false, nullptr); if (res != 0) { ErrorAbort(state, kStashCreationFailure, "mkdir \"%s\" failed: %s", dirname.c_str(), @@ -1399,7 +1412,10 @@ static int PerformCommandDiff(CommandParameters& params) { // We expect the output of the patcher to fill the tgt ranges exactly. if (!writer.Finished()) { - LOG(ERROR) << "range sink underrun?"; + LOG(ERROR) << "Failed to fully write target blocks (range sink underrun): Missing " + << writer.AvailableSpace() << " bytes"; + failure_type = kPatchApplicationFailure; + return -1; } } else { LOG(INFO) << "skipping " << blocks << " blocks already patched to " << tgt.blocks() << " [" @@ -1445,14 +1461,9 @@ static int PerformCommandErase(CommandParameters& params) { LOG(INFO) << " erasing " << tgt.blocks() << " blocks"; for (const auto& [begin, end] : tgt) { - uint64_t blocks[2]; - // offset in bytes - blocks[0] = begin * static_cast<uint64_t>(BLOCKSIZE); - // length in bytes - blocks[1] = (end - begin) * static_cast<uint64_t>(BLOCKSIZE); - - if (ioctl(params.fd, BLKDISCARD, &blocks) == -1) { - PLOG(ERROR) << "BLKDISCARD ioctl failed"; + off64_t offset = static_cast<off64_t>(begin) * BLOCKSIZE; + size_t size = (end - begin) * BLOCKSIZE; + if (!discard_blocks(params.fd, offset, size, true /* force */)) { return -1; } } @@ -1514,7 +1525,7 @@ static int PerformCommandComputeHashTree(CommandParameters& params) { // Starts the hash_tree computation. HashTreeBuilder builder(BLOCKSIZE, hash_function); - if (!builder.Initialize(source_ranges.blocks() * BLOCKSIZE, salt)) { + if (!builder.Initialize(static_cast<int64_t>(source_ranges.blocks()) * BLOCKSIZE, salt)) { LOG(ERROR) << "Failed to initialize hash tree computation, source " << source_ranges.ToString() << ", salt " << salt_hex; return -1; @@ -1570,6 +1581,43 @@ using CommandFunction = std::function<int(CommandParameters&)>; using CommandMap = std::unordered_map<Command::Type, CommandFunction>; +static bool Sha1DevicePath(const std::string& path, uint8_t digest[SHA_DIGEST_LENGTH]) { + auto device_name = android::base::Basename(path); + auto dm_target_name_path = "/sys/block/" + device_name + "/dm/name"; + + struct stat sb; + if (stat(dm_target_name_path.c_str(), &sb) == 0) { + // This is a device mapper target. Use partition name as part of the hash instead. Do not + // include extents as part of the hash, because the size of a partition may be shrunk after + // the patches are applied. + std::string dm_target_name; + if (!android::base::ReadFileToString(dm_target_name_path, &dm_target_name)) { + PLOG(ERROR) << "Cannot read " << dm_target_name_path; + return false; + } + SHA1(reinterpret_cast<const uint8_t*>(dm_target_name.data()), dm_target_name.size(), digest); + return true; + } + + if (errno != ENOENT) { + // This is a device mapper target, but its name cannot be retrieved. + PLOG(ERROR) << "Cannot get dm target name for " << path; + return false; + } + + // This doesn't appear to be a device mapper target, but if its name starts with dm-, something + // else might have gone wrong. + if (android::base::StartsWith(device_name, "dm-")) { + LOG(WARNING) << "Device " << path << " starts with dm- but is not mapped by device-mapper."; + } + + // Stash directory should be different for each partition to avoid conflicts when updating + // multiple partitions at the same time, so we use the hash of the block device name as the base + // directory. + SHA1(reinterpret_cast<const uint8_t*>(path.data()), path.size(), digest); + return true; +} + static Value* PerformBlockImageUpdate(const char* name, State* state, const std::vector<std::unique_ptr<Expr>>& argv, const CommandMap& command_map, bool dryrun) { @@ -1654,12 +1702,10 @@ static Value* PerformBlockImageUpdate(const char* name, State* state, return StringValue(""); } - // Stash directory should be different for each partition to avoid conflicts when updating - // multiple partitions at the same time, so we use the hash of the block device name as the base - // directory. uint8_t digest[SHA_DIGEST_LENGTH]; - SHA1(reinterpret_cast<const uint8_t*>(blockdev_filename->data.data()), - blockdev_filename->data.size(), digest); + if (!Sha1DevicePath(blockdev_filename->data, digest)) { + return StringValue(""); + } params.stashbase = print_sha1(digest); // Possibly do return early on retry, by checking the marker. If the update on this partition has @@ -1869,8 +1915,10 @@ pbiudone: const char* partition = strrchr(blockdev_filename->data.c_str(), '/'); if (partition != nullptr && *(partition + 1) != 0) { - fprintf(cmd_pipe, "log bytes_written_%s: %zu\n", partition + 1, params.written * BLOCKSIZE); - fprintf(cmd_pipe, "log bytes_stashed_%s: %zu\n", partition + 1, params.stashed * BLOCKSIZE); + fprintf(cmd_pipe, "log bytes_written_%s: %" PRIu64 "\n", partition + 1, + static_cast<uint64_t>(params.written) * BLOCKSIZE); + fprintf(cmd_pipe, "log bytes_stashed_%s: %" PRIu64 "\n", partition + 1, + static_cast<uint64_t>(params.stashed) * BLOCKSIZE); fflush(cmd_pipe); } // Delete stash only after successfully completing the update, as it may contain blocks needed @@ -1881,7 +1929,7 @@ pbiudone: // Create a marker on /cache partition, which allows skipping the update on this partition on // retry. The marker will be removed once booting into normal boot, or before starting next // fresh install. - if (!SetPartitionUpdatedMarker(updated_marker)) { + if (!SetUpdatedMarker(updated_marker)) { LOG(WARNING) << "Failed to set updated marker; continuing"; } } diff --git a/updater/dynamic_partitions.cpp b/updater/dynamic_partitions.cpp new file mode 100644 index 000000000..b50dd75f9 --- /dev/null +++ b/updater/dynamic_partitions.cpp @@ -0,0 +1,435 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "updater/dynamic_partitions.h" + +#include <sys/stat.h> +#include <sys/types.h> + +#include <algorithm> +#include <chrono> +#include <iterator> +#include <memory> +#include <optional> +#include <string> +#include <type_traits> +#include <vector> + +#include <android-base/file.h> +#include <android-base/logging.h> +#include <android-base/parseint.h> +#include <android-base/strings.h> +#include <fs_mgr.h> +#include <fs_mgr_dm_linear.h> +#include <libdm/dm.h> +#include <liblp/builder.h> + +#include "edify/expr.h" +#include "otautil/error_code.h" +#include "otautil/paths.h" +#include "private/utils.h" + +using android::base::ParseUint; +using android::dm::DeviceMapper; +using android::dm::DmDeviceState; +using android::fs_mgr::CreateLogicalPartition; +using android::fs_mgr::DestroyLogicalPartition; +using android::fs_mgr::LpMetadata; +using android::fs_mgr::MetadataBuilder; +using android::fs_mgr::Partition; +using android::fs_mgr::PartitionOpener; + +static constexpr std::chrono::milliseconds kMapTimeout{ 1000 }; +static constexpr char kMetadataUpdatedMarker[] = "/dynamic_partition_metadata.UPDATED"; + +static std::string GetSuperDevice() { + return "/dev/block/by-name/" + fs_mgr_get_super_partition_name(); +} + +static std::vector<std::string> ReadStringArgs(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv, + const std::vector<std::string>& arg_names) { + if (argv.size() != arg_names.size()) { + ErrorAbort(state, kArgsParsingFailure, "%s expects %zu arguments, got %zu", name, + arg_names.size(), argv.size()); + return {}; + } + + std::vector<std::unique_ptr<Value>> args; + if (!ReadValueArgs(state, argv, &args)) { + return {}; + } + + CHECK_EQ(args.size(), arg_names.size()); + + for (size_t i = 0; i < arg_names.size(); ++i) { + if (args[i]->type != Value::Type::STRING) { + ErrorAbort(state, kArgsParsingFailure, "%s argument to %s must be string", + arg_names[i].c_str(), name); + return {}; + } + } + + std::vector<std::string> ret; + std::transform(args.begin(), args.end(), std::back_inserter(ret), + [](const auto& arg) { return arg->data; }); + return ret; +} + +static bool UnmapPartitionOnDeviceMapper(const std::string& partition_name) { + auto state = DeviceMapper::Instance().GetState(partition_name); + if (state == DmDeviceState::INVALID) { + return true; + } + if (state == DmDeviceState::ACTIVE) { + return DestroyLogicalPartition(partition_name, kMapTimeout); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast<std::underlying_type_t<DmDeviceState>>(state); + return false; +} + +static bool MapPartitionOnDeviceMapper(const std::string& partition_name, std::string* path) { + auto state = DeviceMapper::Instance().GetState(partition_name); + if (state == DmDeviceState::INVALID) { + return CreateLogicalPartition(GetSuperDevice(), 0 /* metadata slot */, partition_name, + true /* force writable */, kMapTimeout, path); + } + + if (state == DmDeviceState::ACTIVE) { + return DeviceMapper::Instance().GetDmDevicePathByName(partition_name, path); + } + LOG(ERROR) << "Unknown device mapper state: " + << static_cast<std::underlying_type_t<DmDeviceState>>(state); + return false; +} + +Value* UnmapPartitionFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + return UnmapPartitionOnDeviceMapper(args[0]) ? StringValue("t") : StringValue(""); +} + +Value* MapPartitionFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + auto args = ReadStringArgs(name, state, argv, { "name" }); + if (args.empty()) return StringValue(""); + + std::string path; + bool result = MapPartitionOnDeviceMapper(args[0], &path); + return result ? StringValue(path) : StringValue(""); +} + +namespace { // Ops + +struct OpParameters { + std::vector<std::string> tokens; + MetadataBuilder* builder; + + bool ExpectArgSize(size_t size) const { + CHECK(!tokens.empty()); + auto actual = tokens.size() - 1; + if (actual != size) { + LOG(ERROR) << "Op " << op() << " expects " << size << " args, got " << actual; + return false; + } + return true; + } + const std::string& op() const { + CHECK(!tokens.empty()); + return tokens[0]; + } + const std::string& arg(size_t pos) const { + CHECK_LE(pos + 1, tokens.size()); + return tokens[pos + 1]; + } + std::optional<uint64_t> uint_arg(size_t pos, const std::string& name) const { + auto str = arg(pos); + uint64_t ret; + if (!ParseUint(str, &ret)) { + LOG(ERROR) << "Op " << op() << " expects uint64 for argument " << name << ", got " << str; + return std::nullopt; + } + return ret; + } +}; + +using OpFunction = std::function<bool(const OpParameters&)>; +using OpMap = std::map<std::string, OpFunction>; + +bool PerformOpResize(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + auto size = params.uint_arg(1, "size"); + if (!size.has_value()) return false; + + auto partition = params.builder->FindPartition(partition_name); + if (partition == nullptr) { + LOG(ERROR) << "Failed to find partition " << partition_name + << " in dynamic partition metadata."; + return false; + } + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before resizing."; + return false; + } + if (!params.builder->ResizePartition(partition, size.value())) { + LOG(ERROR) << "Failed to resize partition " << partition_name << " to size " << *size << "."; + return false; + } + return true; +} + +bool PerformOpRemove(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& partition_name = params.arg(0); + + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before removing."; + return false; + } + params.builder->RemovePartition(partition_name); + return true; +} + +bool PerformOpAdd(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + const auto& group_name = params.arg(1); + + if (params.builder->AddPartition(partition_name, group_name, LP_PARTITION_ATTR_READONLY) == + nullptr) { + LOG(ERROR) << "Failed to add partition " << partition_name << " to group " << group_name << "."; + return false; + } + return true; +} + +bool PerformOpMove(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& partition_name = params.arg(0); + const auto& new_group = params.arg(1); + + auto partition = params.builder->FindPartition(partition_name); + if (partition == nullptr) { + LOG(ERROR) << "Cannot move partition " << partition_name << " to group " << new_group + << " because it is not found."; + return false; + } + + auto old_group = partition->group_name(); + if (old_group != new_group) { + if (!params.builder->ChangePartitionGroup(partition, new_group)) { + LOG(ERROR) << "Cannot move partition " << partition_name << " from group " << old_group + << " to group " << new_group << "."; + return false; + } + } + return true; +} + +bool PerformOpAddGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name = params.arg(0); + auto maximum_size = params.uint_arg(1, "maximum_size"); + if (!maximum_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name); + if (group != nullptr) { + LOG(ERROR) << "Cannot add group " << group_name << " because it already exists."; + return false; + } + + if (maximum_size.value() == 0) { + LOG(WARNING) << "Adding group " << group_name << " with no size limits."; + } + + if (!params.builder->AddGroup(group_name, maximum_size.value())) { + LOG(ERROR) << "Failed to add group " << group_name << " with maximum size " + << maximum_size.value() << "."; + return false; + } + return true; +} + +bool PerformOpResizeGroup(const OpParameters& params) { + if (!params.ExpectArgSize(2)) return false; + const auto& group_name = params.arg(0); + auto new_size = params.uint_arg(1, "maximum_size"); + if (!new_size.has_value()) return false; + + auto group = params.builder->FindGroup(group_name); + if (group == nullptr) { + LOG(ERROR) << "Cannot resize group " << group_name << " because it is not found."; + return false; + } + + auto old_size = group->maximum_size(); + if (old_size != new_size.value()) { + if (!params.builder->ChangeGroupSize(group_name, new_size.value())) { + LOG(ERROR) << "Cannot resize group " << group_name << " from " << old_size << " to " + << new_size.value() << "."; + return false; + } + } + return true; +} + +std::vector<std::string> ListPartitionNamesInGroup(MetadataBuilder* builder, + const std::string& group_name) { + auto partitions = builder->ListPartitionsInGroup(group_name); + std::vector<std::string> partition_names; + std::transform(partitions.begin(), partitions.end(), std::back_inserter(partition_names), + [](Partition* partition) { return partition->name(); }); + return partition_names; +} + +bool PerformOpRemoveGroup(const OpParameters& params) { + if (!params.ExpectArgSize(1)) return false; + const auto& group_name = params.arg(0); + + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name); + if (!partition_names.empty()) { + LOG(ERROR) << "Cannot remove group " << group_name << " because it still contains partitions [" + << android::base::Join(partition_names, ", ") << "]"; + return false; + } + params.builder->RemoveGroupAndPartitions(group_name); + return true; +} + +bool PerformOpRemoveAllGroups(const OpParameters& params) { + if (!params.ExpectArgSize(0)) return false; + + auto group_names = params.builder->ListGroups(); + for (const auto& group_name : group_names) { + auto partition_names = ListPartitionNamesInGroup(params.builder, group_name); + for (const auto& partition_name : partition_names) { + if (!UnmapPartitionOnDeviceMapper(partition_name)) { + LOG(ERROR) << "Cannot unmap " << partition_name << " before removing group " << group_name + << "."; + return false; + } + } + params.builder->RemoveGroupAndPartitions(group_name); + } + return true; +} + +} // namespace + +Value* UpdateDynamicPartitionsFn(const char* name, State* state, + const std::vector<std::unique_ptr<Expr>>& argv) { + if (argv.size() != 1) { + ErrorAbort(state, kArgsParsingFailure, "%s expects 1 arguments, got %zu", name, argv.size()); + return StringValue(""); + } + std::vector<std::unique_ptr<Value>> args; + if (!ReadValueArgs(state, argv, &args)) { + return nullptr; + } + const std::unique_ptr<Value>& op_list_value = args[0]; + if (op_list_value->type != Value::Type::BLOB) { + ErrorAbort(state, kArgsParsingFailure, "op_list argument to %s must be blob", name); + return StringValue(""); + } + + std::string updated_marker = Paths::Get().stash_directory_base() + kMetadataUpdatedMarker; + if (state->is_retry) { + struct stat sb; + int result = stat(updated_marker.c_str(), &sb); + if (result == 0) { + LOG(INFO) << "Skipping already updated dynamic partition metadata based on marker"; + return StringValue("t"); + } + } else { + // Delete the obsolete marker if any. + std::string err; + if (!android::base::RemoveFileIfExists(updated_marker, &err)) { + LOG(ERROR) << "Failed to remove dynamic partition metadata updated marker " << updated_marker + << ": " << err; + return StringValue(""); + } + } + + auto super_device = GetSuperDevice(); + auto builder = MetadataBuilder::New(PartitionOpener(), super_device, 0); + if (builder == nullptr) { + LOG(ERROR) << "Failed to load dynamic partition metadata."; + return StringValue(""); + } + + static const OpMap op_map{ + // clang-format off + {"resize", PerformOpResize}, + {"remove", PerformOpRemove}, + {"add", PerformOpAdd}, + {"move", PerformOpMove}, + {"add_group", PerformOpAddGroup}, + {"resize_group", PerformOpResizeGroup}, + {"remove_group", PerformOpRemoveGroup}, + {"remove_all_groups", PerformOpRemoveAllGroups}, + // clang-format on + }; + + std::vector<std::string> lines = android::base::Split(op_list_value->data, "\n"); + for (const auto& line : lines) { + auto comment_idx = line.find('#'); + auto op_and_args = comment_idx == std::string::npos ? line : line.substr(0, comment_idx); + op_and_args = android::base::Trim(op_and_args); + if (op_and_args.empty()) continue; + + auto tokens = android::base::Split(op_and_args, " "); + const auto& op = tokens[0]; + auto it = op_map.find(op); + if (it == op_map.end()) { + LOG(ERROR) << "Unknown operation in op_list: " << op; + return StringValue(""); + } + OpParameters params; + params.tokens = tokens; + params.builder = builder.get(); + if (!it->second(params)) { + return StringValue(""); + } + } + + auto metadata = builder->Export(); + if (metadata == nullptr) { + LOG(ERROR) << "Failed to export metadata."; + return StringValue(""); + } + + if (!UpdatePartitionTable(super_device, *metadata, 0)) { + LOG(ERROR) << "Failed to write metadata."; + return StringValue(""); + } + + if (!SetUpdatedMarker(updated_marker)) { + LOG(ERROR) << "Failed to set metadata updated marker."; + return StringValue(""); + } + + return StringValue("t"); +} + +void RegisterDynamicPartitionsFunctions() { + RegisterFunction("unmap_partition", UnmapPartitionFn); + RegisterFunction("map_partition", MapPartitionFn); + RegisterFunction("update_dynamic_partitions", UpdateDynamicPartitionsFn); +} diff --git a/updater/include/private/utils.h b/updater/include/private/utils.h new file mode 100644 index 000000000..33cf6155d --- /dev/null +++ b/updater/include/private/utils.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <string> + +bool SetUpdatedMarker(const std::string& marker); diff --git a/updater/include/updater/dynamic_partitions.h b/updater/include/updater/dynamic_partitions.h new file mode 100644 index 000000000..31cf859c6 --- /dev/null +++ b/updater/include/updater/dynamic_partitions.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +void RegisterDynamicPartitionsFunctions(); diff --git a/updater/install.cpp b/updater/install.cpp index deb7a2b02..20a204a83 100644 --- a/updater/install.cpp +++ b/updater/install.cpp @@ -393,17 +393,20 @@ Value* UnmountFn(const char* name, State* state, const std::vector<std::unique_p return StringValue(mount_point); } -static int exec_cmd(const char* path, char* const argv[]) { +static int exec_cmd(const std::vector<std::string>& args) { + CHECK(!args.empty()); + auto argv = StringVectorToNullTerminatedArray(args); + pid_t child; if ((child = vfork()) == 0) { - execv(path, argv); + execv(argv[0], argv.data()); _exit(EXIT_FAILURE); } int status; waitpid(child, &status, 0); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { - LOG(ERROR) << path << " failed with status " << WEXITSTATUS(status); + LOG(ERROR) << args[0] << " failed with status " << WEXITSTATUS(status); } return WEXITSTATUS(status); } @@ -453,66 +456,53 @@ Value* FormatFn(const char* name, State* state, const std::vector<std::unique_pt } if (fs_type == "ext4") { - const char* mke2fs_argv[] = { "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", - location.c_str(), nullptr, nullptr }; - std::string size_str; + std::vector<std::string> mke2fs_args = { + "/system/bin/mke2fs", "-t", "ext4", "-b", "4096", location + }; if (size != 0) { - size_str = std::to_string(size / 4096LL); - mke2fs_argv[6] = size_str.c_str(); + mke2fs_args.push_back(std::to_string(size / 4096LL)); } - int status = exec_cmd(mke2fs_argv[0], const_cast<char**>(mke2fs_argv)); - if (status != 0) { + if (auto status = exec_cmd(mke2fs_args); status != 0) { LOG(ERROR) << name << ": mke2fs failed (" << status << ") on " << location; return StringValue(""); } - const char* e2fsdroid_argv[] = { "/system/bin/e2fsdroid", "-e", "-a", mount_point.c_str(), - location.c_str(), nullptr }; - status = exec_cmd(e2fsdroid_argv[0], const_cast<char**>(e2fsdroid_argv)); - if (status != 0) { + if (auto status = exec_cmd({ "/system/bin/e2fsdroid", "-e", "-a", mount_point, location }); + status != 0) { LOG(ERROR) << name << ": e2fsdroid failed (" << status << ") on " << location; return StringValue(""); } return StringValue(location); - } else if (fs_type == "f2fs") { + } + + if (fs_type == "f2fs") { if (size < 0) { LOG(ERROR) << name << ": fs_size can't be negative for f2fs: " << fs_size; return StringValue(""); } - std::string num_sectors = std::to_string(size / 512); - - const char* f2fs_path = "/sbin/mkfs.f2fs"; - const char* f2fs_argv[] = { "mkfs.f2fs", - "-d1", - "-f", - "-O", "encrypt", - "-O", "quota", - "-O", "verity", - "-w", "512", - location.c_str(), - (size < 512) ? nullptr : num_sectors.c_str(), - nullptr }; - int status = exec_cmd(f2fs_path, const_cast<char**>(f2fs_argv)); - if (status != 0) { - LOG(ERROR) << name << ": mkfs.f2fs failed (" << status << ") on " << location; + std::vector<std::string> f2fs_args = { + "/system/bin/make_f2fs", "-g", "android", "-w", "512", location + }; + if (size >= 512) { + f2fs_args.push_back(std::to_string(size / 512)); + } + if (auto status = exec_cmd(f2fs_args); status != 0) { + LOG(ERROR) << name << ": make_f2fs failed (" << status << ") on " << location; return StringValue(""); } - const char* sload_argv[] = { "/sbin/sload.f2fs", "-t", mount_point.c_str(), location.c_str(), - nullptr }; - status = exec_cmd(sload_argv[0], const_cast<char**>(sload_argv)); - if (status != 0) { - LOG(ERROR) << name << ": sload.f2fs failed (" << status << ") on " << location; + if (auto status = exec_cmd({ "/system/bin/sload_f2fs", "-t", mount_point, location }); + status != 0) { + LOG(ERROR) << name << ": sload_f2fs failed (" << status << ") on " << location; return StringValue(""); } return StringValue(location); - } else { - LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \"" - << partition_type << "\""; } + LOG(ERROR) << name << ": unsupported fs_type \"" << fs_type << "\" partition_type \"" + << partition_type << "\""; return nullptr; } @@ -679,17 +669,12 @@ Value* RunProgramFn(const char* name, State* state, const std::vector<std::uniqu return ErrorAbort(state, kArgsParsingFailure, "%s() Failed to parse the argument(s)", name); } - char* args2[argv.size() + 1]; - for (size_t i = 0; i < argv.size(); i++) { - args2[i] = &args[i][0]; - } - args2[argv.size()] = nullptr; - - LOG(INFO) << "about to run program [" << args2[0] << "] with " << argv.size() << " args"; + auto exec_args = StringVectorToNullTerminatedArray(args); + LOG(INFO) << "about to run program [" << exec_args[0] << "] with " << argv.size() << " args"; pid_t child = fork(); if (child == 0) { - execv(args2[0], args2); + execv(exec_args[0], exec_args.data()); PLOG(ERROR) << "run_program: execv failed"; _exit(EXIT_FAILURE); } @@ -913,20 +898,12 @@ Value* Tune2FsFn(const char* name, State* state, const std::vector<std::unique_p return ErrorAbort(state, kArgsParsingFailure, "%s() could not read args", name); } - char* args2[argv.size() + 1]; - // Tune2fs expects the program name as its args[0] - args2[0] = const_cast<char*>(name); - if (args2[0] == nullptr) { - return nullptr; - } - for (size_t i = 0; i < argv.size(); ++i) { - args2[i + 1] = &args[i][0]; - } + // tune2fs expects the program name as its first arg. + args.insert(args.begin(), "tune2fs"); + auto tune2fs_args = StringVectorToNullTerminatedArray(args); - // tune2fs changes the file system parameters on an ext2 file system; it - // returns 0 on success. - int result = tune2fs_main(argv.size() + 1, args2); - if (result != 0) { + // tune2fs changes the filesystem parameters on an ext2 filesystem; it returns 0 on success. + if (auto result = tune2fs_main(tune2fs_args.size() - 1, tune2fs_args.data()); result != 0) { return ErrorAbort(state, kTune2FsFailure, "%s() returned error code %d", name, result); } return StringValue("t"); diff --git a/updater/updater.cpp b/updater/updater.cpp index e87c57a5c..7b5a3f938 100644 --- a/updater/updater.cpp +++ b/updater/updater.cpp @@ -35,6 +35,7 @@ #include "otautil/error_code.h" #include "otautil/sysutil.h" #include "updater/blockimg.h" +#include "updater/dynamic_partitions.h" #include "updater/install.h" // Generated by the makefile, this function defines the @@ -125,6 +126,7 @@ int main(int argc, char** argv) { RegisterBuiltins(); RegisterInstallFunctions(); RegisterBlockImageFunctions(); + RegisterDynamicPartitionsFunctions(); RegisterDeviceExtensions(); // Parse the script. diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml index 18d8425e1..0a2511617 100644 --- a/updater_sample/AndroidManifest.xml +++ b/updater_sample/AndroidManifest.xml @@ -33,7 +33,7 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> - <service android:name=".services.PrepareStreamingService"/> + <service android:name=".services.PrepareUpdateService"/> </application> </manifest> diff --git a/updater_sample/README.md b/updater_sample/README.md index f9c3fb8ec..2070ebc21 100644 --- a/updater_sample/README.md +++ b/updater_sample/README.md @@ -191,7 +191,7 @@ privileged system app, so it's granted the required permissions to access </privapp-permissions> ``` to `frameworks/base/data/etc/privapp-permissions-platform.xml` -5. Build sample app `mmma -j bootable/recovery/updater_sample`. +5. Build sample app `make -j SystemUpdaterSample`. 6. Build Android `make -j` 7. [Flash the device](https://source.android.com/setup/build/running) 8. Add update config files; look above at `## Update Config file`; @@ -225,7 +225,9 @@ privileged system app, so it's granted the required permissions to access ## Running tests -1. Build `mmma bootable/recovery/updater_sample/` +The commands are expected to be run from `$ANDROID_BUILD_TOP`. + +1. Build `make -j SystemUpdaterSample` and `make -j SystemUpdaterSampleTests`. 2. Install app `adb install $OUT/system/priv-app/SystemUpdaterSample/SystemUpdaterSample.apk` 3. Install tests @@ -235,8 +237,8 @@ privileged system app, so it's granted the required permissions to access 5. Run a test file ``` adb shell am instrument \ - -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \ - -c com.example.android.systemupdatersample.util.PayloadSpecsTest + -w -e class com.example.android.systemupdatersample.UpdateManagerTest#applyUpdate_appliesPayloadToUpdateEngine \ + com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner ``` diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java index 12a8f3f5f..c02e60846 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java +++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java @@ -17,19 +17,18 @@ package com.example.android.systemupdatersample; import android.content.Context; +import android.os.Handler; import android.os.UpdateEngine; import android.os.UpdateEngineCallback; import android.util.Log; -import com.example.android.systemupdatersample.services.PrepareStreamingService; -import com.example.android.systemupdatersample.util.PayloadSpecs; +import com.example.android.systemupdatersample.services.PrepareUpdateService; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineProperties; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.AtomicDouble; -import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -50,11 +49,10 @@ public class UpdateManager { private static final String TAG = "UpdateManager"; /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */ - private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36"; private final UpdateEngine mUpdateEngine; - private final PayloadSpecs mPayloadSpecs; private AtomicInteger mUpdateEngineStatus = new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE); @@ -84,9 +82,15 @@ public class UpdateManager { private final UpdateManager.UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateManager.UpdateEngineCallbackImpl(); - public UpdateManager(UpdateEngine updateEngine, PayloadSpecs payloadSpecs) { + private final Handler mHandler; + + /** + * @param updateEngine UpdateEngine instance. + * @param handler Handler for {@link PrepareUpdateService} intent service. + */ + public UpdateManager(UpdateEngine updateEngine, Handler handler) { this.mUpdateEngine = updateEngine; - this.mPayloadSpecs = payloadSpecs; + this.mHandler = handler; } /** @@ -293,45 +297,17 @@ public class UpdateManager { mManualSwitchSlotRequired.set(false); } - if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { - applyAbNonStreamingUpdate(config); - } else { - applyAbStreamingUpdate(context, config); - } - } - - private void applyAbNonStreamingUpdate(UpdateConfig config) - throws UpdaterState.InvalidTransitionException { - UpdateData.Builder builder = UpdateData.builder() - .setExtraProperties(prepareExtraProperties(config)); - - try { - builder.setPayload(mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile())); - } catch (IOException e) { - Log.e(TAG, "Error creating payload spec", e); - setUpdaterState(UpdaterState.ERROR); - return; - } - updateEngineApplyPayload(builder.build()); - } - - private void applyAbStreamingUpdate(Context context, UpdateConfig config) { - UpdateData.Builder builder = UpdateData.builder() - .setExtraProperties(prepareExtraProperties(config)); - - Log.d(TAG, "Starting PrepareStreamingService"); - PrepareStreamingService.startService(context, config, (code, payloadSpec) -> { - if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) { - builder.setPayload(payloadSpec); - builder.addExtraProperty("USER_AGENT=" + HTTP_USER_AGENT); - config.getAbConfig() - .getAuthorization() - .ifPresent(s -> builder.addExtraProperty("AUTHORIZATION=" + s)); - updateEngineApplyPayload(builder.build()); - } else { - Log.e(TAG, "PrepareStreamingService failed, result code is " + code); + Log.d(TAG, "Starting PrepareUpdateService"); + PrepareUpdateService.startService(context, config, mHandler, (code, payloadSpec) -> { + if (code != PrepareUpdateService.RESULT_CODE_SUCCESS) { + Log.e(TAG, "PrepareUpdateService failed, result code is " + code); setUpdaterStateSilent(UpdaterState.ERROR); + return; } + updateEngineApplyPayload(UpdateData.builder() + .setExtraProperties(prepareExtraProperties(config)) + .setPayload(payloadSpec) + .build()); }); } @@ -343,6 +319,12 @@ public class UpdateManager { // User will enable it manually by clicking "Switch Slot" button on the screen. extraProperties.add(UpdateEngineProperties.PROPERTY_DISABLE_SWITCH_SLOT_ON_REBOOT); } + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_STREAMING) { + extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT); + config.getAbConfig() + .getAuthorization() + .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s)); + } return extraProperties; } @@ -497,14 +479,14 @@ public class UpdateManager { * system/update_engine/binder_service_android.cc in * function BinderUpdateEngineAndroidService::bind). * - * @param status one of {@link UpdateEngine.UpdateStatusConstants}. + * @param status one of {@link UpdateEngine.UpdateStatusConstants}. * @param progress a number from 0.0 to 1.0. */ private void onStatusUpdate(int status, float progress) { Log.d(TAG, String.format( - "onStatusUpdate invoked, status=%s, progress=%.2f", - status, - progress)); + "onStatusUpdate invoked, status=%s, progress=%.2f", + status, + progress)); int previousStatus = mUpdateEngineStatus.get(); mUpdateEngineStatus.set(status); @@ -555,7 +537,6 @@ public class UpdateManager { } /** - * * Contains update data - PayloadSpec and extra properties list. * * <p>{@code mPayload} contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}. diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java index 931404857..29eb13da7 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java +++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareUpdateService.java @@ -28,6 +28,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.RecoverySystem; import android.os.ResultReceiver; +import android.os.UpdateEngine; import android.util.Log; import com.example.android.systemupdatersample.PayloadSpec; @@ -41,7 +42,9 @@ import com.google.common.collect.ImmutableSet; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Arrays; import java.util.Optional; /** @@ -49,10 +52,10 @@ import java.util.Optional; * without downloading the whole package. And it constructs {@link PayloadSpec}. * All this work required to install streaming A/B updates. * - * PrepareStreamingService runs on it's own thread. It will notify activity + * PrepareUpdateService runs on it's own thread. It will notify activity * using interface {@link UpdateResultCallback} when update is ready to install. */ -public class PrepareStreamingService extends IntentService { +public class PrepareUpdateService extends IntentService { /** * UpdateResultCallback result codes. @@ -61,62 +64,63 @@ public class PrepareStreamingService extends IntentService { public static final int RESULT_CODE_ERROR = 1; /** - * This interface is used to send results from {@link PrepareStreamingService} to + * Extra params that will be sent to IntentService. + */ + public static final String EXTRA_PARAM_CONFIG = "config"; + public static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver"; + + /** + * This interface is used to send results from {@link PrepareUpdateService} to * {@code MainActivity}. */ public interface UpdateResultCallback { - /** * Invoked when files are downloaded and payload spec is constructed. * - * @param resultCode result code, values are defined in {@link PrepareStreamingService} + * @param resultCode result code, values are defined in {@link PrepareUpdateService} * @param payloadSpec prepared payload spec for streaming update */ void onReceiveResult(int resultCode, PayloadSpec payloadSpec); } /** - * Starts PrepareStreamingService. + * Starts PrepareUpdateService. * - * @param context application context - * @param config update config + * @param context application context + * @param config update config * @param resultCallback callback that will be called when the update is ready to be installed */ public static void startService(Context context, UpdateConfig config, + Handler handler, UpdateResultCallback resultCallback) { - Log.d(TAG, "Starting PrepareStreamingService"); - ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback); - Intent intent = new Intent(context, PrepareStreamingService.class); + Log.d(TAG, "Starting PrepareUpdateService"); + ResultReceiver receiver = new CallbackResultReceiver(handler, resultCallback); + Intent intent = new Intent(context, PrepareUpdateService.class); intent.putExtra(EXTRA_PARAM_CONFIG, config); intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver); context.startService(intent); } - public PrepareStreamingService() { + public PrepareUpdateService() { super(TAG); } - private static final String TAG = "PrepareStreamingService"; - - /** - * Extra params that will be sent from Activity to IntentService. - */ - private static final String EXTRA_PARAM_CONFIG = "config"; - private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver"; + private static final String TAG = "PrepareUpdateService"; /** * The files that should be downloaded before streaming. */ private static final ImmutableSet<String> PRE_STREAMING_FILES_SET = ImmutableSet.of( - PackageFiles.CARE_MAP_FILE_NAME, - PackageFiles.COMPATIBILITY_ZIP_FILE_NAME, - PackageFiles.METADATA_FILE_NAME, - PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME + PackageFiles.CARE_MAP_FILE_NAME, + PackageFiles.COMPATIBILITY_ZIP_FILE_NAME, + PackageFiles.METADATA_FILE_NAME, + PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME ); private final PayloadSpecs mPayloadSpecs = new PayloadSpecs(); + private final UpdateEngine mUpdateEngine = new UpdateEngine(); @Override protected void onHandleIntent(Intent intent) { @@ -142,6 +146,17 @@ public class PrepareStreamingService extends IntentService { private PayloadSpec execute(UpdateConfig config) throws IOException, PreparationFailedException { + if (config.getAbConfig().getVerifyPayloadMetadata()) { + Log.i(TAG, "Verifying payload metadata with UpdateEngine."); + if (!verifyPayloadMetadata(config)) { + throw new PreparationFailedException("Payload metadata is not compatible"); + } + } + + if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) { + return mPayloadSpecs.forNonStreaming(config.getUpdatePackageFile()); + } + downloadPreStreamingFiles(config, OTA_PACKAGE_DIR); Optional<UpdateConfig.PackageFile> payloadBinary = @@ -173,9 +188,52 @@ public class PrepareStreamingService extends IntentService { } /** + * Downloads only payload_metadata.bin and verifies with + * {@link UpdateEngine#verifyPayloadMetadata}. + * Returns {@code true} if the payload is verified or the result is unknown because of + * exception from UpdateEngine. + * By downloading only small portion of the package, it allows to verify if UpdateEngine + * will install the update. + */ + private boolean verifyPayloadMetadata(UpdateConfig config) { + Optional<UpdateConfig.PackageFile> metadataPackageFile = + Arrays.stream(config.getAbConfig().getPropertyFiles()) + .filter(p -> p.getFilename().equals( + PackageFiles.PAYLOAD_METADATA_FILE_NAME)) + .findFirst(); + if (!metadataPackageFile.isPresent()) { + Log.w(TAG, String.format("ab_config.property_files doesn't contain %s", + PackageFiles.PAYLOAD_METADATA_FILE_NAME)); + return true; + } + Path metadataPath = Paths.get(OTA_PACKAGE_DIR, PackageFiles.PAYLOAD_METADATA_FILE_NAME); + try { + Files.deleteIfExists(metadataPath); + FileDownloader d = new FileDownloader( + config.getUrl(), + metadataPackageFile.get().getOffset(), + metadataPackageFile.get().getSize(), + metadataPath.toFile()); + d.download(); + } catch (IOException e) { + Log.w(TAG, String.format("Downloading %s from %s failed", + PackageFiles.PAYLOAD_METADATA_FILE_NAME, + config.getUrl()), e); + return true; + } + try { + return mUpdateEngine.verifyPayloadMetadata(metadataPath.toAbsolutePath().toString()); + } catch (Exception e) { + Log.w(TAG, "UpdateEngine#verifyPayloadMetadata failed", e); + return true; + } + } + + /** * Downloads files defined in {@link UpdateConfig#getAbConfig()} * and exists in {@code PRE_STREAMING_FILES_SET}, and put them * in directory {@code dir}. + * * @throws IOException when can't download a file */ private void downloadPreStreamingFiles(UpdateConfig config, String dir) @@ -212,7 +270,7 @@ public class PrepareStreamingService extends IntentService { } /** - * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec} + * Used by {@link PrepareUpdateService} to pass {@link PayloadSpec} * to {@link UpdateResultCallback#onReceiveResult}. */ private static class CallbackResultReceiver extends ResultReceiver { diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java index fc9fddd70..6d1e4c35a 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java +++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java @@ -21,6 +21,7 @@ import android.app.AlertDialog; import android.graphics.Color; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.UpdateEngine; import android.util.Log; import android.view.View; @@ -34,7 +35,6 @@ import com.example.android.systemupdatersample.R; import com.example.android.systemupdatersample.UpdateConfig; import com.example.android.systemupdatersample.UpdateManager; import com.example.android.systemupdatersample.UpdaterState; -import com.example.android.systemupdatersample.util.PayloadSpecs; import com.example.android.systemupdatersample.util.UpdateConfigs; import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes; import com.example.android.systemupdatersample.util.UpdateEngineStatuses; @@ -67,7 +67,7 @@ public class MainActivity extends Activity { private List<UpdateConfig> mConfigs; private final UpdateManager mUpdateManager = - new UpdateManager(new UpdateEngine(), new PayloadSpecs()); + new UpdateManager(new UpdateEngine(), new Handler()); @Override protected void onCreate(Bundle savedInstanceState) { diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java index ddd0919b8..0f9083d27 100644 --- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java +++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java @@ -30,7 +30,7 @@ import java.net.URLConnection; * Downloads chunk of a file from given url using {@code offset} and {@code size}, * and saves to a given location. * - * In real-life application this helper class should download from HTTP Server, + * In a real-life application this helper class should download from HTTP Server, * but in this sample app it will only download from a local file. */ public final class FileDownloader { diff --git a/updater_sample/tests/Android.bp b/updater_sample/tests/Android.bp index c2783ef88..7867770a0 100644 --- a/updater_sample/tests/Android.bp +++ b/updater_sample/tests/Android.bp @@ -28,7 +28,7 @@ android_test { "guava", ], - instrumentation_for: "com.example.android.systemupdatersample", + instrumentation_for: "SystemUpdaterSample", optimize: { enabled: false, diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java index e05ad290c..5ad16d477 100644 --- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java +++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java @@ -18,20 +18,25 @@ package com.example.android.systemupdatersample; import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; import android.os.UpdateEngine; import android.os.UpdateEngineCallback; import android.support.test.InstrumentationRegistry; +import android.support.test.annotation.UiThreadTest; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import com.example.android.systemupdatersample.services.PrepareUpdateService; import com.example.android.systemupdatersample.tests.R; -import com.example.android.systemupdatersample.util.PayloadSpecs; import com.google.common.collect.ImmutableList; import com.google.common.io.CharStreams; @@ -43,7 +48,6 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; -import java.io.File; import java.io.IOException; import java.io.InputStreamReader; @@ -60,49 +64,39 @@ public class UpdateManagerTest { @Mock private UpdateEngine mUpdateEngine; @Mock - private PayloadSpecs mPayloadSpecs; + private Context mMockContext; private UpdateManager mSubject; - private Context mContext; - private UpdateConfig mNonStreamingUpdate003; + private Context mTestContext; + private UpdateConfig mStreamingUpdate002; @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getContext(); - mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs); - mNonStreamingUpdate003 = - UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream)); + mTestContext = InstrumentationRegistry.getContext(); + mSubject = new UpdateManager(mUpdateEngine, null); + mStreamingUpdate002 = + UpdateConfig.fromJson(readResource(R.raw.update_config_002_stream)); } @Test public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception { - PayloadSpec payload = buildMockPayloadSpec(); - when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload); - when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { - // When UpdateManager is bound to update_engine, it passes - // UpdateEngineCallback as a callback to update_engine. - UpdateEngineCallback callback = answer.getArgument(0); - callback.onStatusUpdate( - UpdateEngine.UpdateStatusConstants.IDLE, - /*engineProgress*/ 0.0f); - return null; - }); - - mSubject.bind(); - mSubject.applyUpdate(null, mNonStreamingUpdate003); + mockContextStartServiceAnswer(buildMockPayloadSpec()); + mSubject.applyUpdate(mMockContext, mStreamingUpdate002); verify(mUpdateEngine).applyPayload( "file://blah", 120, 340, - new String[] { - "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false + new String[]{ + "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false + "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT }); } @Test - public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Exception { - PayloadSpec payload = buildMockPayloadSpec(); - when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload); + @UiThreadTest + public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Throwable { + mockContextStartServiceAnswer(buildMockPayloadSpec()); + // UpdateEngine always returns IDLE status. when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> { // When UpdateManager is bound to update_engine, it passes // UpdateEngineCallback as a callback to update_engine. @@ -114,21 +108,36 @@ public class UpdateManagerTest { }); mSubject.bind(); - mSubject.applyUpdate(null, mNonStreamingUpdate003); + mSubject.applyUpdate(mMockContext, mStreamingUpdate002); mSubject.unbind(); mSubject.bind(); // re-bind - now it should re-apply last update assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING); - // it should be called 2 times verify(mUpdateEngine, times(2)).applyPayload( "file://blah", 120, 340, - new String[] { - "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false + new String[]{ + "SWITCH_SLOT_ON_REBOOT=0", // ab_config.force_switch_slot = false + "USER_AGENT=" + UpdateManager.HTTP_USER_AGENT }); } + private void mockContextStartServiceAnswer(PayloadSpec payloadSpec) { + doAnswer(args -> { + Intent intent = args.getArgument(0); + ResultReceiver resultReceiver = intent.getParcelableExtra( + PrepareUpdateService.EXTRA_PARAM_RESULT_RECEIVER); + Bundle b = new Bundle(); + b.putSerializable( + /* PrepareUpdateService.CallbackResultReceiver.BUNDLE_PARAM_PAYLOAD_SPEC */ + "payload-spec", + payloadSpec); + resultReceiver.send(PrepareUpdateService.RESULT_CODE_SUCCESS, b); + return null; + }).when(mMockContext).startService(any(Intent.class)); + } + private PayloadSpec buildMockPayloadSpec() { PayloadSpec payload = mock(PayloadSpec.class); when(payload.getUrl()).thenReturn("file://blah"); @@ -140,7 +149,7 @@ public class UpdateManagerTest { private String readResource(int id) throws IOException { return CharStreams.toString(new InputStreamReader( - mContext.getResources().openRawResource(id))); + mTestContext.getResources().openRawResource(id))); } } |