Snap for 4748783 from 1ad0f9962a to qt-release

Change-Id: Ia88de16c9ddb1bd119a2d12c525277751aea0a12
This commit is contained in:
android-build-team Robot
2018-04-27 09:33:14 +00:00
52 changed files with 2382 additions and 343 deletions
+1 -1
View File
@@ -211,7 +211,7 @@ include $(BUILD_EXECUTABLE)
include \
$(LOCAL_PATH)/boot_control/Android.mk \
$(LOCAL_PATH)/minui/Android.mk \
$(LOCAL_PATH)/sample_updater/Android.mk \
$(LOCAL_PATH)/tests/Android.mk \
$(LOCAL_PATH)/tools/Android.mk \
$(LOCAL_PATH)/updater/Android.mk \
$(LOCAL_PATH)/updater_sample/Android.mk \
+1 -1
View File
@@ -7,5 +7,5 @@ clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
-fw sample_updater/
-fw updater_sample/
+2 -2
View File
@@ -70,7 +70,7 @@ static void maybe_restart_adbd() {
}
}
int apply_from_adb(bool* wipe_cache, const char* install_file) {
int apply_from_adb(bool* wipe_cache) {
modified_flash = true;
stop_adbd();
@@ -113,7 +113,7 @@ int apply_from_adb(bool* wipe_cache, const char* install_file) {
break;
}
}
result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, install_file, false, 0);
result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0);
break;
}
+1 -1
View File
@@ -17,6 +17,6 @@
#ifndef _ADB_INSTALL_H
#define _ADB_INSTALL_H
int apply_from_adb(bool* wipe_cache, const char* install_file);
int apply_from_adb(bool* wipe_cache);
#endif
+10 -5
View File
@@ -40,7 +40,7 @@
#include "edify/expr.h"
#include "otafault/ota_io.h"
#include "otautil/cache_location.h"
#include "otautil/paths.h"
#include "otautil/print_sha1.h"
static int LoadPartitionContents(const std::string& filename, FileContents* file);
@@ -403,7 +403,7 @@ int applypatch_check(const char* filename, const std::vector<std::string>& patch
// If the source file is missing or corrupted, it might be because we were killed in the middle
// of patching it. A copy of it should have been made in cache_temp_source. If that file
// exists and matches the sha1 we're looking for, the check still passes.
if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), &file) != 0) {
if (LoadFileContents(Paths::Get().cache_temp_source().c_str(), &file) != 0) {
printf("failed to load cache file\n");
return 1;
}
@@ -525,7 +525,7 @@ int applypatch(const char* source_filename, const char* target_filename,
printf("source file is bad; trying copy\n");
FileContents copy_file;
if (LoadFileContents(CacheLocation::location().cache_temp_source().c_str(), &copy_file) < 0) {
if (LoadFileContents(Paths::Get().cache_temp_source().c_str(), &copy_file) < 0) {
printf("failed to read copy file\n");
return 1;
}
@@ -620,7 +620,7 @@ static int GenerateTarget(const FileContents& source_file, const std::unique_ptr
printf("not enough free space on /cache\n");
return 1;
}
if (SaveFileContents(CacheLocation::location().cache_temp_source().c_str(), &source_file) < 0) {
if (SaveFileContents(Paths::Get().cache_temp_source().c_str(), &source_file) < 0) {
printf("failed to back up source file\n");
return 1;
}
@@ -630,6 +630,11 @@ static int GenerateTarget(const FileContents& source_file, const std::unique_ptr
SHA_CTX ctx;
SHA1_Init(&ctx);
SinkFn sink = [&memory_sink_str, &ctx](const unsigned char* data, size_t len) {
if (len != 0) {
uint8_t digest[SHA_DIGEST_LENGTH];
SHA1(data, len, digest);
LOG(DEBUG) << "Appending " << len << " bytes data, sha1: " << short_sha1(digest);
}
SHA1_Update(&ctx, data, len);
memory_sink_str.append(reinterpret_cast<const char*>(data), len);
return len;
@@ -680,7 +685,7 @@ static int GenerateTarget(const FileContents& source_file, const std::unique_ptr
}
// Delete the backup copy of the source.
unlink(CacheLocation::location().cache_temp_source().c_str());
unlink(Paths::Get().cache_temp_source().c_str());
// Success!
return 0;
+4 -4
View File
@@ -38,7 +38,7 @@
#include <android-base/strings.h>
#include "applypatch/applypatch.h"
#include "otautil/cache_location.h"
#include "otautil/paths.h"
static int EliminateOpenFiles(const std::string& dirname, std::set<std::string>* files) {
std::unique_ptr<DIR, decltype(&closedir)> d(opendir("/proc"), closedir);
@@ -95,7 +95,7 @@ static std::vector<std::string> FindExpendableFiles(
// We can't delete cache_temp_source; if it's there we might have restarted during
// installation and could be depending on it to be there.
if (path == CacheLocation::location().cache_temp_source()) {
if (path == Paths::Get().cache_temp_source()) {
continue;
}
@@ -142,7 +142,7 @@ int MakeFreeSpaceOnCache(size_t bytes_needed) {
return 0;
#endif
std::vector<std::string> dirs = { "/cache", CacheLocation::location().cache_log_directory() };
std::vector<std::string> dirs = { "/cache", Paths::Get().cache_log_directory() };
for (const auto& dirname : dirs) {
if (RemoveFilesInDirectory(bytes_needed, dirname, FreeSpaceForFile)) {
return 0;
@@ -172,7 +172,7 @@ bool RemoveFilesInDirectory(size_t bytes_needed, const std::string& dirname,
}
std::vector<std::string> files;
if (dirname == CacheLocation::location().cache_log_directory()) {
if (dirname == Paths::Get().cache_log_directory()) {
// Deletes the log files only.
auto log_filter = [](const std::string& file_name) {
return android::base::StartsWith(file_name, "last_log") ||
+28 -1
View File
@@ -38,6 +38,7 @@
#include <zlib.h>
#include "edify/expr.h"
#include "otautil/print_sha1.h"
static inline int64_t Read8(const void *address) {
return android::base::get_unaligned<int64_t>(address);
@@ -76,8 +77,10 @@ static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_
size_t actual_target_length = 0;
size_t total_written = 0;
static constexpr size_t buffer_size = 32768;
SHA_CTX sha_ctx;
SHA1_Init(&sha_ctx);
auto compression_sink = [&strm, &actual_target_length, &expected_target_length, &total_written,
&ret, &sink](const uint8_t* data, size_t len) -> size_t {
&ret, &sink, &sha_ctx](const uint8_t* data, size_t len) -> size_t {
// The input patch length for an update never exceeds INT_MAX.
strm.avail_in = len;
strm.next_in = data;
@@ -98,6 +101,20 @@ static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_
size_t have = buffer_size - strm.avail_out;
total_written += have;
// TODO(b/67849209) Remove after debugging the unit test flakiness.
if (android::base::GetMinimumLogSeverity() <= android::base::LogSeverity::DEBUG &&
have != 0) {
SHA1_Update(&sha_ctx, data, len - strm.avail_in);
SHA_CTX temp_ctx;
memcpy(&temp_ctx, &sha_ctx, sizeof(SHA_CTX));
uint8_t digest_so_far[SHA_DIGEST_LENGTH];
SHA1_Final(digest_so_far, &temp_ctx);
LOG(DEBUG) << "Processed " << actual_target_length + len - strm.avail_in
<< " bytes input data in the sink function, sha1 so far: "
<< short_sha1(digest_so_far);
}
if (sink(buffer.data(), have) != have) {
LOG(ERROR) << "Failed to write " << have << " compressed bytes to output.";
return 0;
@@ -111,6 +128,11 @@ static bool ApplyBSDiffPatchAndStreamOutput(const uint8_t* src_data, size_t src_
int bspatch_result = ApplyBSDiffPatch(src_data, src_len, patch, patch_offset, compression_sink);
deflateEnd(&strm);
if (android::base::GetMinimumLogSeverity() <= android::base::LogSeverity::DEBUG) {
uint8_t digest[SHA_DIGEST_LENGTH];
SHA1_Final(digest, &sha_ctx);
LOG(DEBUG) << "sha1 of " << actual_target_length << " bytes input data: " << short_sha1(digest);
}
if (bspatch_result != 0) {
return false;
}
@@ -182,6 +204,8 @@ int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value&
printf("Failed to apply bsdiff patch.\n");
return -1;
}
LOG(DEBUG) << "Processed chunk type normal";
} else if (type == CHUNK_RAW) {
const char* raw_header = patch_header + pos;
pos += 4;
@@ -201,6 +225,8 @@ int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value&
return -1;
}
pos += data_len;
LOG(DEBUG) << "Processed chunk type raw";
} else if (type == CHUNK_DEFLATE) {
// deflate chunks have an additional 60 bytes in their chunk header.
const char* deflate_header = patch_header + pos;
@@ -276,6 +302,7 @@ int ApplyImagePatch(const unsigned char* old_data, size_t old_size, const Value&
return -1;
}
LOG(DEBUG) << "Processed chunk type deflate";
} else {
printf("patch chunk %d is unknown type %d\n", i, type);
return -1;
+3 -3
View File
@@ -17,8 +17,8 @@
#ifndef RECOVERY_COMMON_H
#define RECOVERY_COMMON_H
#include <stdio.h>
#include <stdarg.h>
#include <stdio.h>
#include <string>
@@ -38,9 +38,9 @@ extern std::string stage;
extern const char* reason;
// fopen a file, mounting volumes and making parent dirs as necessary.
FILE* fopen_path(const char *path, const char *mode);
FILE* fopen_path(const std::string& path, const char* mode);
void ui_print(const char* format, ...);
void ui_print(const char* format, ...) __printflike(1, 2);
bool is_ro_debuggable();
+3 -3
View File
@@ -52,6 +52,7 @@
#include "otautil/SysUtil.h"
#include "otautil/ThermalUtil.h"
#include "otautil/error_code.h"
#include "otautil/paths.h"
#include "private/install.h"
#include "roots.h"
#include "ui.h"
@@ -627,10 +628,8 @@ static int really_install_package(const std::string& path, bool* wipe_cache, boo
return result;
}
int install_package(const std::string& path, bool* wipe_cache, const std::string& install_file,
bool needs_mount, int retry_count) {
int install_package(const std::string& path, bool* wipe_cache, bool needs_mount, int retry_count) {
CHECK(!path.empty());
CHECK(!install_file.empty());
CHECK(wipe_cache != nullptr);
modified_flash = true;
@@ -693,6 +692,7 @@ int install_package(const std::string& path, bool* wipe_cache, const std::string
std::string log_content =
android::base::Join(log_header, "\n") + "\n" + android::base::Join(log_buffer, "\n") + "\n";
const std::string& install_file = Paths::Get().temporary_install_file();
if (!android::base::WriteStringToFile(log_content, install_file)) {
PLOG(ERROR) << "failed to write " << install_file;
}
+5 -2
View File
@@ -17,7 +17,10 @@
#ifndef RECOVERY_INSTALL_H_
#define RECOVERY_INSTALL_H_
#include <stddef.h>
#include <string>
#include <ziparchive/zip_archive.h>
enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SKIPPED,
@@ -25,8 +28,8 @@ enum { INSTALL_SUCCESS, INSTALL_ERROR, INSTALL_CORRUPT, INSTALL_NONE, INSTALL_SK
// Installs the given update package. If INSTALL_SUCCESS is returned and *wipe_cache is true on
// exit, caller should wipe the cache partition.
int install_package(const std::string& package, bool* wipe_cache, const std::string& install_file,
bool needs_mount, int retry_count);
int install_package(const std::string& package, bool* wipe_cache, bool needs_mount,
int retry_count);
// Verify the package by ota keys. Return true if the package is verified successfully,
// otherwise return false.
+1 -1
View File
@@ -21,7 +21,7 @@ cc_library_static {
"SysUtil.cpp",
"DirUtil.cpp",
"ThermalUtil.cpp",
"cache_location.cpp",
"paths.cpp",
"rangeset.cpp",
],
-79
View File
@@ -1,79 +0,0 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
#define _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
#include <string>
#include "android-base/macros.h"
// A singleton class to maintain the update related locations. The locations should be only set
// once at the start of the program.
class CacheLocation {
public:
static CacheLocation& location();
// getter and setter functions.
std::string cache_temp_source() const {
return cache_temp_source_;
}
void set_cache_temp_source(const std::string& temp_source) {
cache_temp_source_ = temp_source;
}
std::string last_command_file() const {
return last_command_file_;
}
void set_last_command_file(const std::string& last_command) {
last_command_file_ = last_command;
}
std::string stash_directory_base() const {
return stash_directory_base_;
}
void set_stash_directory_base(const std::string& base) {
stash_directory_base_ = base;
}
std::string cache_log_directory() const {
return cache_log_directory_;
}
void set_cache_log_directory(const std::string& log_dir) {
cache_log_directory_ = log_dir;
}
private:
CacheLocation();
DISALLOW_COPY_AND_ASSIGN(CacheLocation);
// When there isn't enough room on the target filesystem to hold the patched version of the file,
// we copy the original here and delete it to free up space. If the expected source file doesn't
// exist, or is corrupted, we look to see if the cached file contains the bits we want and use it
// as the source instead. The default location for the cached source is "/cache/saved.file".
std::string cache_temp_source_;
// Location to save the last command that stashes blocks.
std::string last_command_file_;
// The base directory to write stashes during update.
std::string stash_directory_base_;
// The location of last_log & last_kmsg.
std::string cache_log_directory_;
};
#endif // _OTAUTIL_OTAUTIL_CACHE_LOCATION_H_
+98
View File
@@ -0,0 +1,98 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _OTAUTIL_PATHS_H_
#define _OTAUTIL_PATHS_H_
#include <string>
#include <android-base/macros.h>
// A singleton class to maintain the update related paths. The paths should be only set once at the
// start of the program.
class Paths {
public:
static Paths& Get();
std::string cache_log_directory() const {
return cache_log_directory_;
}
void set_cache_log_directory(const std::string& log_dir) {
cache_log_directory_ = log_dir;
}
std::string cache_temp_source() const {
return cache_temp_source_;
}
void set_cache_temp_source(const std::string& temp_source) {
cache_temp_source_ = temp_source;
}
std::string last_command_file() const {
return last_command_file_;
}
void set_last_command_file(const std::string& last_command_file) {
last_command_file_ = last_command_file;
}
std::string stash_directory_base() const {
return stash_directory_base_;
}
void set_stash_directory_base(const std::string& base) {
stash_directory_base_ = base;
}
std::string temporary_install_file() const {
return temporary_install_file_;
}
void set_temporary_install_file(const std::string& install_file) {
temporary_install_file_ = install_file;
}
std::string temporary_log_file() const {
return temporary_log_file_;
}
void set_temporary_log_file(const std::string& log_file) {
temporary_log_file_ = log_file;
}
private:
Paths();
DISALLOW_COPY_AND_ASSIGN(Paths);
// Path to the directory that contains last_log and last_kmsg log files.
std::string cache_log_directory_;
// Path to the temporary source file on /cache. When there isn't enough room on the target
// filesystem to hold the patched version of the file, we copy the original here and delete it to
// free up space. If the expected source file doesn't exist, or is corrupted, we look to see if
// the cached file contains the bits we want and use it as the source instead.
std::string cache_temp_source_;
// Path to the last command file.
std::string last_command_file_;
// Path to the base directory to write stashes during update.
std::string stash_directory_base_;
// Path to the temporary file that contains the install result.
std::string temporary_install_file_;
// Path to the temporary log file while under recovery.
std::string temporary_log_file_;
};
#endif // _OTAUTIL_PATHS_H_
@@ -14,20 +14,24 @@
* limitations under the License.
*/
#include "otautil/cache_location.h"
#include "otautil/paths.h"
constexpr const char kDefaultCacheLogDirectory[] = "/cache/recovery";
constexpr const char kDefaultCacheTempSource[] = "/cache/saved.file";
constexpr const char kDefaultLastCommandFile[] = "/cache/recovery/last_command";
constexpr const char kDefaultStashDirectoryBase[] = "/cache/recovery";
constexpr const char kDefaultCacheLogDirectory[] = "/cache/recovery";
constexpr const char kDefaultTemporaryInstallFile[] = "/tmp/last_install";
constexpr const char kDefaultTemporaryLogFile[] = "/tmp/recovery.log";
CacheLocation& CacheLocation::location() {
static CacheLocation cache_location;
return cache_location;
Paths& Paths::Get() {
static Paths paths;
return paths;
}
CacheLocation::CacheLocation()
: cache_temp_source_(kDefaultCacheTempSource),
Paths::Paths()
: cache_log_directory_(kDefaultCacheLogDirectory),
cache_temp_source_(kDefaultCacheTempSource),
last_command_file_(kDefaultLastCommandFile),
stash_directory_base_(kDefaultStashDirectoryBase),
cache_log_directory_(kDefaultCacheLogDirectory) {}
temporary_install_file_(kDefaultTemporaryInstallFile),
temporary_log_file_(kDefaultTemporaryLogFile) {}
+102 -94
View File
@@ -68,6 +68,7 @@
#include "minui/minui.h"
#include "otautil/DirUtil.h"
#include "otautil/error_code.h"
#include "otautil/paths.h"
#include "roots.h"
#include "rotate_logs.h"
#include "screen_ui.h"
@@ -108,20 +109,13 @@ static const char *CONVERT_FBE_DIR = "/tmp/convert_fbe";
static const char *CONVERT_FBE_FILE = "/tmp/convert_fbe/convert_fbe";
static const char *CACHE_ROOT = "/cache";
static const char *DATA_ROOT = "/data";
static const char* METADATA_ROOT = "/metadata";
static const char *SDCARD_ROOT = "/sdcard";
static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";
static const char *LAST_KMSG_FILE = "/cache/recovery/last_kmsg";
static const char *LAST_LOG_FILE = "/cache/recovery/last_log";
// We will try to apply the update package 5 times at most in case of an I/O error or
// bspatch | imgpatch error.
static const int RETRY_LIMIT = 4;
static const int BATTERY_READ_TIMEOUT_IN_SEC = 10;
// GmsCore enters recovery mode to install package when having enough battery
// percentage. Normally, the threshold is 40% without charger and 20% with charger.
// So we should check battery with a slightly lower limitation.
static const int BATTERY_OK_PERCENTAGE = 20;
static const int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
static constexpr const char* RECOVERY_WIPE = "/etc/recovery.wipe";
static constexpr const char* DEFAULT_LOCALE = "en-US";
@@ -184,8 +178,8 @@ struct selabel_handle* sehandle;
*/
// Open a given path, mounting partitions as necessary.
FILE* fopen_path(const char* path, const char* mode) {
if (ensure_path_mounted(path) != 0) {
FILE* fopen_path(const std::string& path, const char* mode) {
if (ensure_path_mounted(path.c_str()) != 0) {
LOG(ERROR) << "Can't mount " << path;
return nullptr;
}
@@ -195,19 +189,19 @@ FILE* fopen_path(const char* path, const char* mode) {
if (strchr("wa", mode[0])) {
mkdir_recursively(path, 0777, true, sehandle);
}
return fopen(path, mode);
return fopen(path.c_str(), mode);
}
// close a file, log an error if the error indicator is set
static void check_and_fclose(FILE *fp, const char *name) {
fflush(fp);
if (fsync(fileno(fp)) == -1) {
PLOG(ERROR) << "Failed to fsync " << name;
}
if (ferror(fp)) {
PLOG(ERROR) << "Error in " << name;
}
fclose(fp);
static void check_and_fclose(FILE* fp, const std::string& name) {
fflush(fp);
if (fsync(fileno(fp)) == -1) {
PLOG(ERROR) << "Failed to fsync " << name;
}
if (ferror(fp)) {
PLOG(ERROR) << "Error in " << name;
}
fclose(fp);
}
bool is_ro_debuggable() {
@@ -407,27 +401,27 @@ static void save_kernel_log(const char* destination) {
android::base::WriteStringToFile(buffer, destination);
}
// write content to the current pmsg session.
static ssize_t __pmsg_write(const char *filename, const char *buf, size_t len) {
return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO,
filename, buf, len);
// Writes content to the current pmsg session.
static ssize_t __pmsg_write(const std::string& filename, const std::string& buf) {
return __android_log_pmsg_file_write(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filename.c_str(),
buf.data(), buf.size());
}
static void copy_log_file_to_pmsg(const char* source, const char* destination) {
std::string content;
android::base::ReadFileToString(source, &content);
__pmsg_write(destination, content.c_str(), content.length());
static void copy_log_file_to_pmsg(const std::string& source, const std::string& destination) {
std::string content;
android::base::ReadFileToString(source, &content);
__pmsg_write(destination, content);
}
// How much of the temp log we have copied to the copy in cache.
static off_t tmplog_offset = 0;
static void copy_log_file(const char* source, const char* destination, bool append) {
static void copy_log_file(const std::string& source, const std::string& destination, bool append) {
FILE* dest_fp = fopen_path(destination, append ? "ae" : "we");
if (dest_fp == nullptr) {
PLOG(ERROR) << "Can't open " << destination;
} else {
FILE* source_fp = fopen(source, "re");
FILE* source_fp = fopen(source.c_str(), "re");
if (source_fp != nullptr) {
if (append) {
fseeko(source_fp, tmplog_offset, SEEK_SET); // Since last write
@@ -447,39 +441,38 @@ static void copy_log_file(const char* source, const char* destination, bool appe
}
static void copy_logs() {
// We only rotate and record the log of the current session if there are
// actual attempts to modify the flash, such as wipes, installs from BCB
// or menu selections. This is to avoid unnecessary rotation (and
// possible deletion) of log files, if it does not do anything loggable.
if (!modified_flash) {
return;
}
// We only rotate and record the log of the current session if there are actual attempts to modify
// the flash, such as wipes, installs from BCB or menu selections. This is to avoid unnecessary
// rotation (and possible deletion) of log files, if it does not do anything loggable.
if (!modified_flash) {
return;
}
// Always write to pmsg, this allows the OTA logs to be caught in logcat -L
copy_log_file_to_pmsg(TEMPORARY_LOG_FILE, LAST_LOG_FILE);
copy_log_file_to_pmsg(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE);
// Always write to pmsg, this allows the OTA logs to be caught in `logcat -L`.
copy_log_file_to_pmsg(Paths::Get().temporary_log_file(), LAST_LOG_FILE);
copy_log_file_to_pmsg(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE);
// We can do nothing for now if there's no /cache partition.
if (!has_cache) {
return;
}
// We can do nothing for now if there's no /cache partition.
if (!has_cache) {
return;
}
ensure_path_mounted(LAST_LOG_FILE);
ensure_path_mounted(LAST_KMSG_FILE);
rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
ensure_path_mounted(LAST_LOG_FILE);
ensure_path_mounted(LAST_KMSG_FILE);
rotate_logs(LAST_LOG_FILE, LAST_KMSG_FILE);
// Copy logs to cache so the system can find out what happened.
copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
save_kernel_log(LAST_KMSG_FILE);
chmod(LOG_FILE, 0600);
chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
chmod(LAST_KMSG_FILE, 0600);
chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
chmod(LAST_LOG_FILE, 0640);
chmod(LAST_INSTALL_FILE, 0644);
sync();
// Copy logs to cache so the system can find out what happened.
copy_log_file(Paths::Get().temporary_log_file(), LOG_FILE, true);
copy_log_file(Paths::Get().temporary_log_file(), LAST_LOG_FILE, false);
copy_log_file(Paths::Get().temporary_install_file(), LAST_INSTALL_FILE, false);
save_kernel_log(LAST_KMSG_FILE);
chmod(LOG_FILE, 0600);
chown(LOG_FILE, AID_SYSTEM, AID_SYSTEM);
chmod(LAST_KMSG_FILE, 0600);
chown(LAST_KMSG_FILE, AID_SYSTEM, AID_SYSTEM);
chmod(LAST_LOG_FILE, 0640);
chmod(LAST_INSTALL_FILE, 0644);
sync();
}
// Clear the recovery command and prepare to boot a (hopefully working) system,
@@ -752,11 +745,19 @@ static bool wipe_data(Device* device) {
modified_flash = true;
ui->Print("\n-- Wiping data...\n");
bool success =
device->PreWipeData() &&
erase_volume("/data") &&
(has_cache ? erase_volume("/cache") : true) &&
device->PostWipeData();
bool success = device->PreWipeData();
if (success) {
success &= erase_volume(DATA_ROOT);
if (has_cache) {
success &= erase_volume(CACHE_ROOT);
}
if (volume_for_mount_point(METADATA_ROOT) != nullptr) {
success &= erase_volume(METADATA_ROOT);
}
}
if (success) {
success &= device->PostWipeData();
}
ui->Print("Data wipe %s.\n", success ? "complete" : "failed");
return success;
}
@@ -957,10 +958,10 @@ static void choose_recovery_file(Device* device) {
}
} else {
// If cache partition is not found, view /tmp/recovery.log instead.
if (access(TEMPORARY_LOG_FILE, R_OK) == -1) {
if (access(Paths::Get().temporary_log_file().c_str(), R_OK) == -1) {
return;
} else {
entries.push_back(TEMPORARY_LOG_FILE);
entries.push_back(Paths::Get().temporary_log_file());
}
}
@@ -1082,8 +1083,7 @@ static int apply_from_sdcard(Device* device, bool* wipe_cache) {
}
}
result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache,
TEMPORARY_INSTALL_FILE, false, 0/*retry_count*/);
result = install_package(FUSE_SIDELOAD_HOST_PATHNAME, wipe_cache, false, 0 /*retry_count*/);
break;
}
@@ -1160,7 +1160,7 @@ static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
{
bool adb = (chosen_action == Device::APPLY_ADB_SIDELOAD);
if (adb) {
status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
status = apply_from_adb(&should_wipe_cache);
} else {
status = apply_from_sdcard(device, &should_wipe_cache);
}
@@ -1259,7 +1259,7 @@ void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity
}
}
static bool is_battery_ok() {
static bool is_battery_ok(int* required_battery_level) {
using android::hardware::health::V1_0::BatteryStatus;
using android::hardware::health::V2_0::Result;
using android::hardware::health::V2_0::toString;
@@ -1278,14 +1278,15 @@ static bool is_battery_ok() {
.batteryChargeCounterPath = android::String8(android::String8::kEmptyString),
.batteryFullChargePath = android::String8(android::String8::kEmptyString),
.batteryCycleCountPath = android::String8(android::String8::kEmptyString),
.energyCounter = NULL,
.energyCounter = nullptr,
.boot_min_cap = 0,
.screen_on = NULL
.screen_on = nullptr
};
auto health =
android::hardware::health::V2_0::implementation::Health::initInstance(&healthd_config);
static constexpr int BATTERY_READ_TIMEOUT_IN_SEC = 10;
int wait_second = 0;
while (true) {
auto charge_status = BatteryStatus::UNKNOWN;
@@ -1328,9 +1329,15 @@ static bool is_battery_ok() {
if (res != Result::SUCCESS) {
capacity = 100;
}
return (charged && capacity >= BATTERY_WITH_CHARGER_OK_PERCENTAGE) ||
(!charged && capacity >= BATTERY_OK_PERCENTAGE);
}
// GmsCore enters recovery mode to install package when having enough battery percentage.
// Normally, the threshold is 40% without charger and 20% with charger. So we should check
// battery with a slightly lower limitation.
static constexpr int BATTERY_OK_PERCENTAGE = 20;
static constexpr int BATTERY_WITH_CHARGER_OK_PERCENTAGE = 15;
*required_battery_level = charged ? BATTERY_WITH_CHARGER_OK_PERCENTAGE : BATTERY_OK_PERCENTAGE;
return capacity >= *required_battery_level;
}
}
// Set the retry count to |retry_count| in BCB.
@@ -1362,19 +1369,20 @@ static bool bootreason_in_blacklist() {
return false;
}
static void log_failure_code(ErrorCode code, const char *update_package) {
std::vector<std::string> log_buffer = {
update_package,
"0", // install result
"error: " + std::to_string(code),
};
std::string log_content = android::base::Join(log_buffer, "\n");
if (!android::base::WriteStringToFile(log_content, TEMPORARY_INSTALL_FILE)) {
PLOG(ERROR) << "failed to write " << TEMPORARY_INSTALL_FILE;
}
static void log_failure_code(ErrorCode code, const std::string& update_package) {
std::vector<std::string> log_buffer = {
update_package,
"0", // install result
"error: " + std::to_string(code),
};
std::string log_content = android::base::Join(log_buffer, "\n");
const std::string& install_file = Paths::Get().temporary_install_file();
if (!android::base::WriteStringToFile(log_content, install_file)) {
PLOG(ERROR) << "Failed to write " << install_file;
}
// Also write the info into last_log.
LOG(INFO) << log_content;
// Also write the info into last_log.
LOG(INFO) << log_content;
}
int main(int argc, char **argv) {
@@ -1407,7 +1415,7 @@ int main(int argc, char **argv) {
// redirect_stdio should be called only in non-sideload mode. Otherwise
// we may have two logger instances with different timestamps.
redirect_stdio(TEMPORARY_LOG_FILE);
redirect_stdio(Paths::Get().temporary_log_file().c_str());
printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
@@ -1557,9 +1565,10 @@ int main(int argc, char **argv) {
// to log the update attempt since update_package is non-NULL.
modified_flash = true;
if (retry_count == 0 && !is_battery_ok()) {
ui->Print("battery capacity is not enough for installing package, needed is %d%%\n",
BATTERY_OK_PERCENTAGE);
int required_battery_level;
if (retry_count == 0 && !is_battery_ok(&required_battery_level)) {
ui->Print("battery capacity is not enough for installing package: %d%% needed\n",
required_battery_level);
// Log the error code to last_install when installation skips due to
// low battery.
log_failure_code(kLowBattery, update_package);
@@ -1576,8 +1585,7 @@ int main(int argc, char **argv) {
set_retry_bootloader_message(retry_count + 1, args);
}
status = install_package(update_package, &should_wipe_cache, TEMPORARY_INSTALL_FILE, true,
retry_count);
status = install_package(update_package, &should_wipe_cache, true, retry_count);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
wipe_cache(false, device);
}
@@ -1638,7 +1646,7 @@ int main(int argc, char **argv) {
if (!sideload_auto_reboot) {
ui->ShowText(true);
}
status = apply_from_adb(&should_wipe_cache, TEMPORARY_INSTALL_FILE);
status = apply_from_adb(&should_wipe_cache);
if (status == INSTALL_SUCCESS && should_wipe_cache) {
if (!wipe_cache(false, device)) {
status = INSTALL_ERROR;
-1
View File
@@ -1 +0,0 @@
# System update sample app.
@@ -1,20 +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.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
@@ -1,68 +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.android.update.ui;
import android.app.Activity;
import android.os.UpdateEngine;
import android.os.UpdateEngineCallback;
/** Main update activity. */
public class SystemUpdateActivity extends Activity {
private UpdateEngine updateEngine;
private UpdateEngineCallbackImpl updateEngineCallbackImpl = new UpdateEngineCallbackImpl(this);
@Override
public void onResume() {
super.onResume();
updateEngine = new UpdateEngine();
updateEngine.bind(updateEngineCallbackImpl);
}
@Override
public void onPause() {
updateEngine.unbind();
super.onPause();
}
void onStatusUpdate(int i, float v) {
// Handle update engine status update
}
void onPayloadApplicationComplete(int i) {
// Handle apply payload completion
}
private static class UpdateEngineCallbackImpl extends UpdateEngineCallback {
private final SystemUpdateActivity activity;
public UpdateEngineCallbackImpl(SystemUpdateActivity activity) {
this.activity = activity;
}
@Override
public void onStatusUpdate(int i, float v) {
activity.onStatusUpdate(i, v);
}
@Override
public void onPayloadApplicationComplete(int i) {
activity.onPayloadApplicationComplete(i);
}
}
}
+69 -8
View File
@@ -16,7 +16,6 @@
#include <dirent.h>
#include <fcntl.h>
#include <gtest/gtest.h>
#include <libgen.h>
#include <stdio.h>
#include <stdlib.h>
@@ -31,22 +30,66 @@
#include <vector>
#include <android-base/file.h>
#include <android-base/logging.h>
#include <android-base/stringprintf.h>
#include <android-base/test_utils.h>
#include <android-base/unique_fd.h>
#include <bsdiff/bsdiff.h>
#include <gtest/gtest.h>
#include <openssl/sha.h>
#include <zlib.h>
#include "applypatch/applypatch.h"
#include "applypatch/applypatch_modes.h"
#include "common/test_constants.h"
#include "otautil/cache_location.h"
#include "otautil/paths.h"
#include "otautil/print_sha1.h"
using namespace std::string_literals;
// TODO(b/67849209) Remove after debug the flakiness.
static void DecompressAndDumpRecoveryImage(const std::string& image_path) {
// Expected recovery_image structure
// chunk normal: 45066 bytes
// chunk deflate: 479442 bytes
// chunk normal: 5199 bytes
std::string recovery_content;
ASSERT_TRUE(android::base::ReadFileToString(image_path, &recovery_content));
ASSERT_GT(recovery_content.size(), 45066 + 5199);
z_stream strm = {};
strm.avail_in = recovery_content.size() - 45066 - 5199;
strm.next_in =
const_cast<uint8_t*>(reinterpret_cast<const uint8_t*>(recovery_content.data())) + 45066;
ASSERT_EQ(Z_OK, inflateInit2(&strm, -15));
constexpr unsigned int BUFFER_SIZE = 32768;
std::vector<uint8_t> uncompressed_data(BUFFER_SIZE);
size_t uncompressed_length = 0;
SHA_CTX ctx;
SHA1_Init(&ctx);
int ret;
do {
strm.avail_out = BUFFER_SIZE;
strm.next_out = uncompressed_data.data();
ret = inflate(&strm, Z_NO_FLUSH);
ASSERT_GE(ret, 0);
SHA1_Update(&ctx, uncompressed_data.data(), BUFFER_SIZE - strm.avail_out);
uncompressed_length += BUFFER_SIZE - strm.avail_out;
} while (ret != Z_STREAM_END);
inflateEnd(&strm);
uint8_t digest[SHA_DIGEST_LENGTH];
SHA1_Final(digest, &ctx);
GTEST_LOG_(INFO) << "uncompressed length " << uncompressed_length
<< " sha1: " << short_sha1(digest);
}
static void sha1sum(const std::string& fname, std::string* sha1, size_t* fsize = nullptr) {
ASSERT_NE(nullptr, sha1);
ASSERT_TRUE(sha1 != nullptr);
std::string data;
ASSERT_TRUE(android::base::ReadFileToString(fname, &data));
@@ -68,6 +111,14 @@ static void mangle_file(const std::string& fname) {
ASSERT_TRUE(android::base::WriteStringToFile(content, fname));
}
static void test_logger(android::base::LogId /* id */, android::base::LogSeverity severity,
const char* /* tag */, const char* /* file */, unsigned int /* line */,
const char* message) {
if (severity >= android::base::GetMinimumLogSeverity()) {
fprintf(stdout, "%s\n", message);
}
}
class ApplyPatchTest : public ::testing::Test {
public:
virtual void SetUp() override {
@@ -101,14 +152,16 @@ class ApplyPatchCacheTest : public ApplyPatchTest {
protected:
void SetUp() override {
ApplyPatchTest::SetUp();
CacheLocation::location().set_cache_temp_source(old_file);
Paths::Get().set_cache_temp_source(old_file);
}
};
class ApplyPatchModesTest : public ::testing::Test {
protected:
void SetUp() override {
CacheLocation::location().set_cache_temp_source(cache_source.path);
Paths::Get().set_cache_temp_source(cache_source.path);
android::base::InitLogging(nullptr, &test_logger);
android::base::SetMinimumLogSeverity(android::base::LogSeverity::DEBUG);
}
TemporaryFile cache_source;
@@ -146,7 +199,7 @@ class FreeCacheTest : public ::testing::Test {
}
void SetUp() override {
CacheLocation::location().set_cache_log_directory(mock_log_dir.path);
Paths::Get().set_cache_log_directory(mock_log_dir.path);
}
// A mock method to calculate the free space. It assumes the partition has a total size of 40960
@@ -306,7 +359,11 @@ TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithoutBonusFile) {
recovery_img_sha1.c_str(),
recovery_img_size_arg.c_str(),
patch_arg.c_str() };
ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
if (applypatch_modes(args.size(), args.data()) != 0) {
DecompressAndDumpRecoveryImage(tgt_file.path);
FAIL();
}
}
TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithMultiplePatches) {
@@ -349,7 +406,11 @@ TEST_F(ApplyPatchModesTest, PatchModeEmmcTargetWithMultiplePatches) {
for (const auto& arg : args) {
printf(" %s\n", arg);
}
ASSERT_EQ(0, applypatch_modes(args.size(), args.data()));
if (applypatch_modes(args.size(), args.data()) != 0) {
DecompressAndDumpRecoveryImage(tgt_file.path);
FAIL();
}
}
// Ensures that applypatch works with a bsdiff based recovery-from-boot.p.
+7 -8
View File
@@ -41,8 +41,8 @@
#include "common/test_constants.h"
#include "edify/expr.h"
#include "otautil/SysUtil.h"
#include "otautil/cache_location.h"
#include "otautil/error_code.h"
#include "otautil/paths.h"
#include "otautil/print_sha1.h"
#include "updater/blockimg.h"
#include "updater/install.h"
@@ -106,10 +106,9 @@ class UpdaterTest : public ::testing::Test {
RegisterInstallFunctions();
RegisterBlockImageFunctions();
// Mock the location of last_command_file.
CacheLocation::location().set_cache_temp_source(temp_saved_source_.path);
CacheLocation::location().set_last_command_file(temp_last_command_.path);
CacheLocation::location().set_stash_directory_base(temp_stash_base_.path);
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);
}
TemporaryFile temp_saved_source_;
@@ -719,7 +718,7 @@ TEST_F(UpdaterTest, brotli_new_data) {
}
TEST_F(UpdaterTest, last_command_update) {
std::string last_command_file = CacheLocation::location().last_command_file();
std::string last_command_file = Paths::Get().last_command_file();
std::string block1 = std::string(4096, '1');
std::string block2 = std::string(4096, '2');
@@ -806,7 +805,7 @@ TEST_F(UpdaterTest, last_command_update) {
}
TEST_F(UpdaterTest, last_command_update_unresumable) {
std::string last_command_file = CacheLocation::location().last_command_file();
std::string last_command_file = Paths::Get().last_command_file();
std::string block1 = std::string(4096, '1');
std::string block2 = std::string(4096, '2');
@@ -861,7 +860,7 @@ TEST_F(UpdaterTest, last_command_update_unresumable) {
}
TEST_F(UpdaterTest, last_command_verify) {
std::string last_command_file = CacheLocation::location().last_command_file();
std::string last_command_file = Paths::Get().last_command_file();
std::string block1 = std::string(4096, '1');
std::string block2 = std::string(4096, '2');
+13 -17
View File
@@ -15,8 +15,8 @@
*/
#include <ctype.h>
#include <errno.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <linux/fs.h>
@@ -25,13 +25,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <time.h>
#include <unistd.h>
#include <fec/io.h>
#include <functional>
#include <limits>
@@ -47,14 +46,15 @@
#include <android-base/unique_fd.h>
#include <applypatch/applypatch.h>
#include <brotli/decode.h>
#include <fec/io.h>
#include <openssl/sha.h>
#include <private/android_filesystem_config.h>
#include <ziparchive/zip_archive.h>
#include "edify/expr.h"
#include "otafault/ota_io.h"
#include "otautil/cache_location.h"
#include "otautil/error_code.h"
#include "otautil/paths.h"
#include "otautil/print_sha1.h"
#include "otautil/rangeset.h"
#include "updater/install.h"
@@ -74,7 +74,7 @@ static bool is_retry = false;
static std::unordered_map<std::string, RangeSet> stash_map;
static void DeleteLastCommandFile() {
std::string last_command_file = CacheLocation::location().last_command_file();
const std::string& last_command_file = Paths::Get().last_command_file();
if (unlink(last_command_file.c_str()) == -1 && errno != ENOENT) {
PLOG(ERROR) << "Failed to unlink: " << last_command_file;
}
@@ -83,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) {
std::string last_command_file = CacheLocation::location().last_command_file();
const std::string& last_command_file = Paths::Get().last_command_file();
android::base::unique_fd fd(TEMP_FAILURE_RETRY(open(last_command_file.c_str(), O_RDONLY)));
if (fd == -1) {
if (errno != ENOENT) {
@@ -119,7 +119,7 @@ static bool ParseLastCommandFile(int* last_command_index) {
// Update the last command index in the last_command_file if the current command writes to the
// stash either explicitly or implicitly.
static bool UpdateLastCommandIndex(int command_index, const std::string& command_string) {
std::string last_command_file = CacheLocation::location().last_command_file();
const std::string& last_command_file = Paths::Get().last_command_file();
std::string last_command_tmp = last_command_file + ".tmp";
std::string content = std::to_string(command_index) + "\n" + command_string;
android::base::unique_fd wfd(
@@ -672,15 +672,11 @@ static int VerifyBlocks(const std::string& expected, const std::vector<uint8_t>&
}
static std::string GetStashFileName(const std::string& base, const std::string& id,
const std::string& postfix) {
if (base.empty()) {
return "";
}
std::string fn(CacheLocation::location().stash_directory_base());
fn += "/" + base + "/" + id + postfix;
return fn;
const std::string& postfix) {
if (base.empty()) {
return "";
}
return Paths::Get().stash_directory_base() + "/" + base + "/" + id + postfix;
}
// Does a best effort enumeration of stash files. Ignores possible non-file items in the stash
@@ -1697,7 +1693,7 @@ static Value* PerformBlockImageUpdate(const char* name, State* state,
for (size_t i = 0; i < cmdcount; ++i) {
if (cmd_map.find(commands[i].name) != cmd_map.end()) {
LOG(ERROR) << "Error: command [" << commands[i].name << "] already exists in the cmd map.";
return StringValue(strdup(""));
return StringValue("");
}
cmd_map[commands[i].name] = &commands[i];
}
+1 -2
View File
@@ -17,9 +17,9 @@
#include "updater/updater.h"
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <string>
@@ -34,7 +34,6 @@
#include "otafault/config.h"
#include "otautil/DirUtil.h"
#include "otautil/SysUtil.h"
#include "otautil/cache_location.h"
#include "otautil/error_code.h"
#include "updater/blockimg.h"
#include "updater/install.h"
+10
View File
@@ -0,0 +1,10 @@
*~
*.bak
*.pyc
*.pyc-2.4
Thumbs.db
*.iml
.idea/
gen/
.vscode
local.properties
@@ -15,13 +15,18 @@
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := SystemUpdateApp
LOCAL_PACKAGE_NAME := SystemUpdaterSample
LOCAL_SDK_VERSION := system_current
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_TAGS := samples
# TODO: enable proguard and use proguard.flags file
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-java-files-under, src)
include $(BUILD_PACKAGE)
# Use the following include to make our test apk.
include $(call all-makefiles-under,$(LOCAL_PATH))
@@ -15,17 +15,22 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.update">
package="com.example.android.systemupdatersample">
<application android:label="Sample Updater">
<activity android:name=".ui.SystemUpdateActivity"
android:label="Sample Updater">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
<application
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round">
<activity
android:name=".ui.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
+98
View File
@@ -0,0 +1,98 @@
# SystemUpdaterSample
This app demonstrates how to use Android system updates APIs to install
[OTA updates](https://source.android.com/devices/tech/ota/). It contains a sample
client for `update_engine` to install A/B (seamless) updates and a sample of
applying non-A/B updates using `recovery`.
A/B (seamless) update is available since Android Nougat (API 24), but this sample
targets the latest android.
## Workflow
SystemUpdaterSample app shows list of available updates on the UI. User is allowed
to select an update and apply it to the device. App shows installation progress,
logs can be found in `adb logcat`. User can stop or reset an update. Resetting
the update requests update engine to cancel any ongoing update, and revert
if the update has been applied. Stopping does not revert the applied update.
## Update Config file
In this sample updates are defined in JSON update config files.
The structure of a config file is defined in
`com.example.android.systemupdatersample.UpdateConfig`, example file is located
at `res/raw/sample.json`.
In real-life update system the config files expected to be served from a server
to the app, but in this sample, the config files are stored on the device.
The directory can be found in logs or on the UI. In most cases it should be located at
`/data/user/0/com.example.android.systemupdatersample/files/configs/`.
SystemUpdaterSample app downloads OTA package from `url`. If `ab_install_type`
is `NON_STREAMING` then app downloads the whole package and
passes it to the `update_engine`. If `ab_install_type` is `STREAMING`
then app downloads only some files to prepare the streaming update and
`update_engine` will stream only `payload.bin`.
To support streaming A/B (seamless) update, OTA package file must be
an uncompressed (ZIP_STORED) zip file.
Config files can be generated using `tools/gen_update_config.py`.
Running `./tools/gen_update_config.py --help` shows usage of the script.
## Running on a device
The commands expected to be run from `$ANDROID_BUILD_TOP`.
1. Compile the app `$ mmma bootable/recovery/updater_sample`.
2. Install the app to the device using `$ adb install <APK_PATH>`.
3. Add update config files.
## Development
- [x] Create a UI with list of configs, current version,
control buttons, progress bar and log viewer
- [x] Add `PayloadSpec` and `PayloadSpecs` for working with
update zip file
- [x] Add `UpdateConfig` for working with json config files
- [x] Add applying non-streaming update
- [ ] Prepare streaming update (partially downloading package)
- [ ] Add applying streaming update
- [ ] Add tests for `MainActivity`
- [ ] Add stop/reset the update
- [ ] Verify system partition checksum for package
- [ ] HAL compatibility check
- [ ] Change partition demo
- [ ] Add non-A/B updates demo
## Running tests
1. Build `$ mmma bootable/recovery/updater_sample/`
2. Install app
`$ adb install $OUT/system/app/SystemUpdaterSample/SystemUpdaterSample.apk`
3. Install tests
`$ adb install $OUT/testcases/SystemUpdaterSampleTests/SystemUpdaterSampleTests.apk`
4. Run tests
`$ adb shell am instrument -w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner`
5. Run a test file
```
$ adb shell am instrument \
-w com.example.android.systemupdatersample.tests/android.support.test.runner.AndroidJUnitRunner \
-c com.example.android.systemupdatersample.util.PayloadSpecsTest
```
## Getting access to `update_engine` API and read/write access to `/data`
Run adb shell as a root, and set SELinux mode to permissive (0):
```txt
$ adb root
$ adb shell
# setenforce 0
# getenforce
```
+163
View File
@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="4dip"
android:gravity="center_horizontal"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical">
<TextView
android:id="@+id/textViewBuildtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Current Build:" />
<TextView
android:id="@+id/textViewBuild"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/unknown" />
<Space
android:layout_width="match_parent"
android:layout_height="40dp" />
<TextView
android:id="@+id/textView4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Apply an update" />
<TextView
android:id="@+id/textViewConfigsDirHint"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="Config files located in NULL"
android:textColor="#777"
android:textSize="10sp"
android:textStyle="italic" />
<Spinner
android:id="@+id/spinnerConfigs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:orientation="horizontal">
<Button
android:id="@+id/buttonReload"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onReloadClick"
android:text="Reload" />
<Button
android:id="@+id/buttonViewConfig"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onViewConfigClick"
android:text="View config" />
<Button
android:id="@+id/buttonApplyConfig"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onApplyConfigClick"
android:text="Apply" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:orientation="horizontal">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Running update status:" />
<TextView
android:id="@+id/textViewStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:text="@string/unknown" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_marginTop="8dp"
android:min="0"
android:max="100"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<Button
android:id="@+id/buttonStop"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onStopClick"
android:text="Stop" />
<Button
android:id="@+id/buttonReset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onResetClick"
android:text="Reset" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

+22
View File
@@ -0,0 +1,22 @@
{
"__name": "name will be visible on UI",
"__url": "https:// or file:// uri to update file (zip, xz, ...)",
"__type": "NON_STREAMING (from local file) OR STREAMING (on the fly)",
"name": "SAMPLE-cake-release BUILD-12345",
"url": "file:///data/builds/android-update.zip",
"type": "NON_STREAMING",
"streaming_metadata": {
"__": "streaming_metadata is required only for streaming update",
"__property_files": "name, offset and size of files",
"property_files": [
{
"__filename": "payload.bin and payload_properties.txt are required",
"__offset": "defines beginning of update data in archive",
"__size": "size of the update data in archive",
"filename": "payload.bin",
"offset": 531,
"size": 5012323
}
]
}
}
+21
View File
@@ -0,0 +1,21 @@
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">SystemUpdaterSample</string>
<string name="action_reload">Reload</string>
<string name="unknown">Unknown</string>
<string name="close">CLOSE</string>
</resources>
@@ -0,0 +1,122 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample;
import android.os.UpdateEngine;
import java.util.List;
/**
* Payload that will be given to {@link UpdateEngine#applyPayload)}.
*/
public class PayloadSpec {
/**
* Creates a payload spec {@link Builder}
*/
public static Builder newBuilder() {
return new Builder();
}
private String mUrl;
private long mOffset;
private long mSize;
private List<String> mProperties;
public PayloadSpec(Builder b) {
this.mUrl = b.mUrl;
this.mOffset = b.mOffset;
this.mSize = b.mSize;
this.mProperties = b.mProperties;
}
public String getUrl() {
return mUrl;
}
public long getOffset() {
return mOffset;
}
public long getSize() {
return mSize;
}
public List<String> getProperties() {
return mProperties;
}
/**
* payload spec builder.
*
* <p>Usage:</p>
*
* {@code
* PayloadSpec spec = PayloadSpec.newBuilder()
* .url("url")
* .build();
* }
*/
public static class Builder {
private String mUrl;
private long mOffset;
private long mSize;
private List<String> mProperties;
public Builder() {
}
/**
* set url
*/
public Builder url(String url) {
this.mUrl = url;
return this;
}
/**
* set offset
*/
public Builder offset(long offset) {
this.mOffset = offset;
return this;
}
/**
* set size
*/
public Builder size(long size) {
this.mSize = size;
return this;
}
/**
* set properties
*/
public Builder properties(List<String> properties) {
this.mProperties = properties;
return this;
}
/**
* build {@link PayloadSpec}
*/
public PayloadSpec build() {
return new PayloadSpec(this);
}
}
}
@@ -0,0 +1,183 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample;
import android.os.Parcel;
import android.os.Parcelable;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
/**
* UpdateConfig describes an update. It will be parsed from JSON, which is intended to
* be sent from server to the update app, but in this sample app it will be stored on the device.
*/
public class UpdateConfig implements Parcelable {
public static final int TYPE_NON_STREAMING = 0;
public static final int TYPE_STREAMING = 1;
public static final Parcelable.Creator<UpdateConfig> CREATOR =
new Parcelable.Creator<UpdateConfig>() {
@Override
public UpdateConfig createFromParcel(Parcel source) {
return new UpdateConfig(source);
}
@Override
public UpdateConfig[] newArray(int size) {
return new UpdateConfig[size];
}
};
/** parse update config from json */
public static UpdateConfig fromJson(String json) throws JSONException {
UpdateConfig c = new UpdateConfig();
JSONObject o = new JSONObject(json);
c.mName = o.getString("name");
c.mUrl = o.getString("url");
if (TYPE_NON_STREAMING_JSON.equals(o.getString("type"))) {
c.mInstallType = TYPE_NON_STREAMING;
} else if (TYPE_STREAMING_JSON.equals(o.getString("type"))) {
c.mInstallType = TYPE_STREAMING;
} else {
throw new JSONException("Invalid type, expected either "
+ "NON_STREAMING or STREAMING, got " + o.getString("type"));
}
if (o.has("metadata")) {
c.mMetadata = new Metadata(
o.getJSONObject("metadata").getInt("offset"),
o.getJSONObject("metadata").getInt("size"));
}
c.mRawJson = json;
return c;
}
/**
* these strings are represent types in JSON config files
*/
private static final String TYPE_NON_STREAMING_JSON = "NON_STREAMING";
private static final String TYPE_STREAMING_JSON = "STREAMING";
/** name will be visible on UI */
private String mName;
/** update zip file URI, can be https:// or file:// */
private String mUrl;
/** non-streaming (first saves locally) OR streaming (on the fly) */
private int mInstallType;
/** metadata is required only for streaming update */
private Metadata mMetadata;
private String mRawJson;
protected UpdateConfig() {
}
protected UpdateConfig(Parcel in) {
this.mName = in.readString();
this.mUrl = in.readString();
this.mInstallType = in.readInt();
this.mMetadata = (Metadata) in.readSerializable();
this.mRawJson = in.readString();
}
public UpdateConfig(String name, String url, int installType) {
this.mName = name;
this.mUrl = url;
this.mInstallType = installType;
}
public String getName() {
return mName;
}
public String getUrl() {
return mUrl;
}
public String getRawJson() {
return mRawJson;
}
public int getInstallType() {
return mInstallType;
}
/**
* "url" must be the file located on the device.
*
* @return File object for given url
*/
public File getUpdatePackageFile() {
if (mInstallType != TYPE_NON_STREAMING) {
throw new RuntimeException("Expected non-streaming install type");
}
if (!mUrl.startsWith("file://")) {
throw new RuntimeException("url is expected to start with file://");
}
return new File(mUrl.substring(7, mUrl.length()));
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
dest.writeString(mUrl);
dest.writeInt(mInstallType);
dest.writeSerializable(mMetadata);
dest.writeString(mRawJson);
}
/**
* Metadata for STREAMING update
*/
public static class Metadata implements Serializable {
private static final long serialVersionUID = 31042L;
/** defines beginning of update data in archive */
private long mOffset;
/** size of the update data in archive */
private long mSize;
public Metadata(long offset, long size) {
this.mOffset = offset;
this.mSize = size;
}
public long getOffset() {
return mOffset;
}
public long getSize() {
return mSize;
}
}
}
@@ -0,0 +1,314 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.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;
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.R;
import com.example.android.systemupdatersample.UpdateConfig;
import com.example.android.systemupdatersample.updates.AbNonStreamingUpdate;
import com.example.android.systemupdatersample.util.UpdateConfigs;
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
/**
* UI for SystemUpdaterSample app.
*/
public class MainActivity extends Activity {
private TextView mTextViewBuild;
private Spinner mSpinnerConfigs;
private TextView mTextViewConfigsDirHint;
private Button mButtonReload;
private Button mButtonApplyConfig;
private Button mButtonStop;
private Button mButtonReset;
private ProgressBar mProgressBar;
private TextView mTextViewStatus;
private List<UpdateConfig> mConfigs;
private AtomicInteger mUpdateEngineStatus =
new AtomicInteger(UpdateEngine.UpdateStatusConstants.IDLE);
private UpdateEngine mUpdateEngine = new UpdateEngine();
/**
* Listen to {@code update_engine} events.
*/
private UpdateEngineCallbackImpl mUpdateEngineCallback = new UpdateEngineCallbackImpl();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
this.mTextViewBuild = findViewById(R.id.textViewBuild);
this.mSpinnerConfigs = findViewById(R.id.spinnerConfigs);
this.mTextViewConfigsDirHint = findViewById(R.id.textViewConfigsDirHint);
this.mButtonReload = findViewById(R.id.buttonReload);
this.mButtonApplyConfig = findViewById(R.id.buttonApplyConfig);
this.mButtonStop = findViewById(R.id.buttonStop);
this.mButtonReset = findViewById(R.id.buttonReset);
this.mProgressBar = findViewById(R.id.progressBar);
this.mTextViewStatus = findViewById(R.id.textViewStatus);
this.mUpdateEngine.bind(mUpdateEngineCallback);
this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
uiReset();
loadUpdateConfigs();
}
@Override
protected void onDestroy() {
this.mUpdateEngine.unbind();
super.onDestroy();
}
/**
* reload button is clicked
*/
public void onReloadClick(View view) {
loadUpdateConfigs();
}
/**
* view config button is clicked
*/
public void onViewConfigClick(View view) {
UpdateConfig config = mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
new AlertDialog.Builder(this)
.setTitle(config.getName())
.setMessage(config.getRawJson())
.setPositiveButton(R.string.close, (dialog, id) -> dialog.dismiss())
.show();
}
/**
* apply config button is clicked
*/
public void onApplyConfigClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Apply Update")
.setMessage("Do you really want to apply this update?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiSetUpdating();
applyUpdate(getSelectedConfig());
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
/**
* stop button clicked
*/
public void onStopClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Stop Update")
.setMessage("Do you really want to cancel running update?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiReset();
stopRunningUpdate();
})
.setNegativeButton(android.R.string.cancel, null).show();
}
/**
* reset button clicked
*/
public void onResetClick(View view) {
new AlertDialog.Builder(this)
.setTitle("Reset Update")
.setMessage("Do you really want to cancel running update"
+ " and restore old version?")
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
uiReset();
resetUpdate();
})
.setNegativeButton(android.R.string.cancel, null).show();
}
/**
* 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}.
*/
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);
setUiStatus(status);
Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
.show();
});
}
}
/**
* Invoked when the payload has been applied, whether successfully or
* unsuccessfully. The value of {@code errorCode} will be one of the
* values from {@link UpdateEngine.ErrorCodeConstants}.
*/
private void onPayloadApplicationComplete(int errorCode) {
runOnUiThread(() -> {
final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
? "SUCCESS"
: "FAILURE";
Log.i("UpdateEngine",
"Completed - errorCode="
+ UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+ " " + state);
Toast.makeText(this, "Update completed", Toast.LENGTH_LONG).show();
});
}
/** resets ui */
private void uiReset() {
mTextViewBuild.setText(Build.DISPLAY);
mSpinnerConfigs.setEnabled(true);
mButtonReload.setEnabled(true);
mButtonApplyConfig.setEnabled(true);
mButtonStop.setEnabled(false);
mButtonReset.setEnabled(false);
mProgressBar.setProgress(0);
mProgressBar.setEnabled(false);
mProgressBar.setVisibility(ProgressBar.INVISIBLE);
mTextViewStatus.setText(R.string.unknown);
}
/** sets ui updating mode */
private void uiSetUpdating() {
mTextViewBuild.setText(Build.DISPLAY);
mSpinnerConfigs.setEnabled(false);
mButtonReload.setEnabled(false);
mButtonApplyConfig.setEnabled(false);
mButtonStop.setEnabled(true);
mProgressBar.setEnabled(true);
mButtonReset.setEnabled(true);
mProgressBar.setVisibility(ProgressBar.VISIBLE);
}
/**
* loads json configurations from configs dir that is defined in {@link UpdateConfigs}.
*/
private void loadUpdateConfigs() {
mConfigs = UpdateConfigs.getUpdateConfigs(this);
loadConfigsToSpinner(mConfigs);
}
/**
* @param status update engine status code
*/
private void setUiStatus(int status) {
String statusText = UpdateEngineStatuses.getStatusText(status);
mTextViewStatus.setText(statusText);
}
private void loadConfigsToSpinner(List<UpdateConfig> configs) {
String[] spinnerArray = UpdateConfigs.configsToNames(configs);
ArrayAdapter<String> spinnerArrayAdapter = new ArrayAdapter<>(this,
android.R.layout.simple_spinner_item,
spinnerArray);
spinnerArrayAdapter.setDropDownViewResource(android.R.layout
.simple_spinner_dropdown_item);
mSpinnerConfigs.setAdapter(spinnerArrayAdapter);
}
private UpdateConfig getSelectedConfig() {
return mConfigs.get(mSpinnerConfigs.getSelectedItemPosition());
}
/**
* Applies the given update
*/
private void applyUpdate(UpdateConfig config) {
if (config.getInstallType() == UpdateConfig.TYPE_NON_STREAMING) {
AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config);
try {
update.execute();
} catch (Exception e) {
Log.e("MainActivity", "Error applying the update", e);
Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT)
.show();
}
} else {
Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT)
.show();
}
}
/**
* Requests update engine to stop any ongoing update. If an update has been applied,
* leave it as is.
*/
private void stopRunningUpdate() {
Toast.makeText(this,
"stopRunningUpdate is not implemented",
Toast.LENGTH_SHORT).show();
}
/**
* Resets update engine to IDLE state. Requests to cancel any onging update, or to revert if an
* update has been applied.
*/
private void resetUpdate() {
Toast.makeText(this,
"resetUpdate is not implemented",
Toast.LENGTH_SHORT).show();
}
/**
* Helper class to delegate UpdateEngine 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);
}
}
}
@@ -0,0 +1,52 @@
/*
* 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.updates;
import android.os.UpdateEngine;
import com.example.android.systemupdatersample.PayloadSpec;
import com.example.android.systemupdatersample.UpdateConfig;
import com.example.android.systemupdatersample.util.PayloadSpecs;
/**
* Applies A/B (seamless) non-streaming update.
*/
public class AbNonStreamingUpdate {
private final UpdateEngine mUpdateEngine;
private final UpdateConfig mUpdateConfig;
public AbNonStreamingUpdate(UpdateEngine updateEngine, UpdateConfig config) {
this.mUpdateEngine = updateEngine;
this.mUpdateConfig = config;
}
/**
* Start applying the update. This method doesn't wait until end of the update.
* {@code update_engine} works asynchronously.
*/
public void execute() throws Exception {
PayloadSpec payload = PayloadSpecs.forNonStreaming(mUpdateConfig.getUpdatePackageFile());
mUpdateEngine.applyPayload(
payload.getUrl(),
payload.getOffset(),
payload.getSize(),
payload.getProperties().toArray(new String[0]));
}
}
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
/** Utility class for property files in a package. */
public final class PackagePropertyFiles {
public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
public static final String PAYLOAD_HEADER_FILE_NAME = "payload_header.bin";
public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
/** The zip entry in an A/B OTA package, which will be used by update_verifier. */
public static final String CARE_MAP_FILE_NAME = "care_map.txt";
public static final String METADATA_FILE_NAME = "metadata";
/**
* The zip file that claims the compatibility of the update package to check against the Android
* framework to ensure that the package can be installed on the device.
*/
public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
private PackagePropertyFiles() {}
}
@@ -0,0 +1,117 @@
/*
* 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.annotation.TargetApi;
import android.os.Build;
import com.example.android.systemupdatersample.PayloadSpec;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/** The helper class that creates {@link PayloadSpec}. */
@TargetApi(Build.VERSION_CODES.N)
public final class PayloadSpecs {
/**
* The payload PAYLOAD_ENTRY is stored in the zip package to comply with the Android OTA package
* format. We want to find out the offset of the entry, so that we can pass it over to the A/B
* updater without making an extra copy of the payload.
*
* <p>According to Android docs, the entries are listed in the order in which they appear in the
* zip file. So we enumerate the entries to identify the offset of the payload file.
* http://developer.android.com/reference/java/util/zip/ZipFile.html#entries()
*/
public static PayloadSpec forNonStreaming(File packageFile) throws IOException {
boolean payloadFound = false;
long payloadOffset = 0;
long payloadSize = 0;
List<String> properties = new ArrayList<>();
try (ZipFile zip = new ZipFile(packageFile)) {
Enumeration<? extends ZipEntry> entries = zip.entries();
long offset = 0;
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String name = entry.getName();
// Zip local file header has 30 bytes + filename + sizeof extra field.
// https://en.wikipedia.org/wiki/Zip_(file_format)
long extraSize = entry.getExtra() == null ? 0 : entry.getExtra().length;
offset += 30 + name.length() + extraSize;
if (entry.isDirectory()) {
continue;
}
long length = entry.getCompressedSize();
if (PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
if (entry.getMethod() != ZipEntry.STORED) {
throw new IOException("Invalid compression method.");
}
payloadFound = true;
payloadOffset = offset;
payloadSize = length;
} else if (PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
InputStream inputStream = zip.getInputStream(entry);
if (inputStream != null) {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = br.readLine()) != null) {
properties.add(line);
}
}
}
offset += length;
}
}
if (!payloadFound) {
throw new IOException("Failed to find payload entry in the given package.");
}
return PayloadSpec.newBuilder()
.url("file://" + packageFile.getAbsolutePath())
.offset(payloadOffset)
.size(payloadSize)
.properties(properties)
.build();
}
/**
* Converts an {@link PayloadSpec} to a string.
*/
public static String toString(PayloadSpec payloadSpec) {
return "<PayloadSpec url=" + payloadSpec.getUrl()
+ ", offset=" + payloadSpec.getOffset()
+ ", size=" + payloadSpec.getSize()
+ ", properties=" + Arrays.toString(
payloadSpec.getProperties().toArray(new String[0]))
+ ">";
}
private PayloadSpecs() {}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
import android.content.Context;
import com.example.android.systemupdatersample.UpdateConfig;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* Utility class for working with json update configurations.
*/
public final class UpdateConfigs {
private static final String UPDATE_CONFIGS_ROOT = "configs/";
/**
* @param configs update configs
* @return list of names
*/
public static String[] configsToNames(List<UpdateConfig> configs) {
return configs.stream().map(UpdateConfig::getName).toArray(String[]::new);
}
/**
* @param context app context
* @return configs root directory
*/
public static String getConfigsRoot(Context context) {
return Paths.get(context.getFilesDir().toString(),
UPDATE_CONFIGS_ROOT).toString();
}
/**
* It parses only {@code .json} files.
*
* @param context application context
* @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
*/
public static List<UpdateConfig> getUpdateConfigs(Context context) {
File root = new File(getConfigsRoot(context));
ArrayList<UpdateConfig> configs = new ArrayList<>();
if (!root.exists()) {
return configs;
}
for (final File f : root.listFiles()) {
if (!f.isDirectory() && f.getName().endsWith(".json")) {
try {
String json = new String(Files.readAllBytes(f.toPath()),
StandardCharsets.UTF_8);
configs.add(UpdateConfig.fromJson(json));
} catch (Exception e) {
throw new RuntimeException(
"Can't read/parse config file " + f.getName(), e);
}
}
}
return configs;
}
private UpdateConfigs() {}
}
@@ -0,0 +1,84 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
import android.os.UpdateEngine;
import android.util.SparseArray;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Helper class to work with update_engine's error codes.
* Many error codes are defined in {@link UpdateEngine.ErrorCodeConstants},
* but you can find more in system/update_engine/common/error_code.h.
*/
public final class UpdateEngineErrorCodes {
/**
* Error code from the update engine. Values must agree with the ones in
* system/update_engine/common/error_code.h.
*/
public static final int UPDATED_BUT_NOT_ACTIVE = 52;
private static final SparseArray<String> CODE_TO_NAME_MAP = new SparseArray<>();
static {
CODE_TO_NAME_MAP.put(0, "SUCCESS");
CODE_TO_NAME_MAP.put(1, "ERROR");
CODE_TO_NAME_MAP.put(4, "FILESYSTEM_COPIER_ERROR");
CODE_TO_NAME_MAP.put(5, "POST_INSTALL_RUNNER_ERROR");
CODE_TO_NAME_MAP.put(6, "PAYLOAD_MISMATCHED_TYPE_ERROR");
CODE_TO_NAME_MAP.put(7, "INSTALL_DEVICE_OPEN_ERROR");
CODE_TO_NAME_MAP.put(8, "KERNEL_DEVICE_OPEN_ERROR");
CODE_TO_NAME_MAP.put(9, "DOWNLOAD_TRANSFER_ERROR");
CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
}
/**
* Completion codes returned by update engine indicating that the update
* was successfully applied.
*/
private static final Set<Integer> SUCCEEDED_COMPLETION_CODES = new HashSet<Integer>(
Arrays.asList(UpdateEngine.ErrorCodeConstants.SUCCESS,
// UPDATED_BUT_NOT_ACTIVE is returned when the payload is
// successfully applied but the
// device won't switch to the new slot after the next boot.
UPDATED_BUT_NOT_ACTIVE));
/**
* checks if update succeeded using errorCode
*/
public static boolean isUpdateSucceeded(int errorCode) {
return SUCCEEDED_COMPLETION_CODES.contains(errorCode);
}
/**
* converts error code to error name
*/
public static String getCodeName(int errorCode) {
return CODE_TO_NAME_MAP.get(errorCode);
}
private UpdateEngineErrorCodes() {}
}
@@ -0,0 +1,51 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
import android.util.SparseArray;
/**
* Helper class to work with update_engine's error codes.
* Many error codes are defined in {@link UpdateEngine.UpdateStatusConstants},
* but you can find more in system/update_engine/common/error_code.h.
*/
public final class UpdateEngineStatuses {
private static final SparseArray<String> STATUS_MAP = new SparseArray<>();
static {
STATUS_MAP.put(0, "IDLE");
STATUS_MAP.put(1, "CHECKING_FOR_UPDATE");
STATUS_MAP.put(2, "UPDATE_AVAILABLE");
STATUS_MAP.put(3, "DOWNLOADING");
STATUS_MAP.put(4, "VERIFYING");
STATUS_MAP.put(5, "FINALIZING");
STATUS_MAP.put(6, "UPDATED_NEED_REBOOT");
STATUS_MAP.put(7, "REPORTING_ERROR_EVENT");
STATUS_MAP.put(8, "ATTEMPTING_ROLLBACK");
STATUS_MAP.put(9, "DISABLED");
}
/**
* converts status code to status name
*/
public static String getStatusText(int status) {
return STATUS_MAP.get(status);
}
private UpdateEngineStatuses() {}
}
+32
View File
@@ -0,0 +1,32 @@
#
# 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.
#
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := SystemUpdaterSampleTests
LOCAL_SDK_VERSION := system_current
LOCAL_MODULE_TAGS := tests
LOCAL_JAVA_LIBRARIES := \
android.test.base.stubs \
android.test.runner.stubs
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample
LOCAL_PROGUARD_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-subdir-java-files)
include $(BUILD_PACKAGE)
+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2018 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.systemupdatersample.tests">
<!-- We add an application tag here just so that we can indicate that
this package needs to link against the android.test library,
which is needed when building test cases. -->
<application>
<uses-library android:name="android.test.runner" />
</application>
<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="com.example.android.systemupdatersample"
android:label="Tests for SystemUpdaterSample."/>
</manifest>
+1
View File
@@ -0,0 +1 @@
tested.project.dir=..
Binary file not shown.
@@ -0,0 +1,14 @@
{
"name": "streaming-001",
"url": "http://foo.bar/update.zip",
"type": "STREAMING",
"streaming_metadata": {
"property_files": [
{
"filename": "payload.bin",
"offset": 531,
"size": 5012323
}
]
}
}
@@ -0,0 +1,35 @@
{
"__": "*** Generated using tools/gen_update_config.py ***",
"ab_install_type": "STREAMING",
"ab_streaming_metadata": {
"property_files": [
{
"filename": "payload.bin",
"offset": 41,
"size": 7
},
{
"filename": "payload_properties.txt",
"offset": 100,
"size": 18
},
{
"filename": "care_map.txt",
"offset": 160,
"size": 8
},
{
"filename": "compatibility.zip",
"offset": 215,
"size": 13
},
{
"filename": "metadata",
"offset": 287,
"size": 8
}
]
},
"name": "S ota_002_package",
"url": "file:///data/sample-ota-packages/ota_002_package.zip"
}
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
/**
* Tests for {@link UpdateConfig}
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UpdateConfigTest {
private static final String JSON_NON_STREAMING =
"{\"name\": \"vip update\", \"url\": \"file:///builds/a.zip\", "
+ " \"type\": \"NON_STREAMING\"}";
private static final String JSON_STREAMING =
"{\"name\": \"vip update 2\", \"url\": \"http://foo.bar/a.zip\", "
+ "\"type\": \"STREAMING\"}";
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void fromJson_parsesJsonConfigWithoutMetadata() throws Exception {
UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
assertEquals("name is parsed", "vip update", config.getName());
assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
assertSame("type is parsed", UpdateConfig.TYPE_NON_STREAMING, config.getInstallType());
assertEquals("url is parsed", "file:///builds/a.zip", config.getUrl());
}
@Test
public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
UpdateConfig config = UpdateConfig.fromJson(JSON_STREAMING);
thrown.expect(RuntimeException.class);
config.getUpdatePackageFile();
}
@Test
public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\","
+ " \"type\": \"NON_STREAMING\"}";
UpdateConfig config = UpdateConfig.fromJson(json);
thrown.expect(RuntimeException.class);
config.getUpdatePackageFile();
}
@Test
public void getUpdatePackageFile_works() throws Exception {
UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
assertEquals("correct path", "/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
}
}
@@ -0,0 +1,48 @@
/*
* 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());
}
}
@@ -0,0 +1,117 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
import static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME;
import static com.example.android.systemupdatersample.util.PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.systemupdatersample.PayloadSpec;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
/**
* Tests if PayloadSpecs parses update package zip file correctly.
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class PayloadSpecsTest {
private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
private static final String PAYLOAD_CONTENTS = "hello\nworld";
private static final int PAYLOAD_SIZE = PAYLOAD_CONTENTS.length();
private File mTestDir;
private Context mContext;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
mTestDir = mContext.getFilesDir();
}
@Test
public void forNonStreaming_works() throws Exception {
File packageFile = createMockZipFile();
PayloadSpec spec = PayloadSpecs.forNonStreaming(packageFile);
assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
assertEquals("correct payload offset",
30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
assertEquals("correct payload size", PAYLOAD_SIZE, spec.getSize());
assertArrayEquals("correct properties",
new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
}
@Test
public void forNonStreaming_IOException() throws Exception {
thrown.expect(IOException.class);
PayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
}
/**
* Creates package zip file that contains payload.bin and payload_properties.txt
*/
private File createMockZipFile() throws IOException {
File testFile = new File(mTestDir, "test.zip");
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(testFile))) {
// Add payload.bin entry.
ZipEntry entry = new ZipEntry(PAYLOAD_BINARY_FILE_NAME);
entry.setMethod(ZipEntry.STORED);
entry.setCompressedSize(PAYLOAD_SIZE);
entry.setSize(PAYLOAD_SIZE);
CRC32 crc = new CRC32();
crc.update(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
entry.setCrc(crc.getValue());
zos.putNextEntry(entry);
zos.write(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
// Add payload properties entry.
ZipEntry propertiesEntry = new ZipEntry(PAYLOAD_PROPERTIES_FILE_NAME);
zos.putNextEntry(propertiesEntry);
zos.write(PROPERTIES_CONTENTS.getBytes(StandardCharsets.UTF_8));
zos.closeEntry();
}
return testFile;
}
}
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.android.systemupdatersample.util;
import static org.junit.Assert.assertArrayEquals;
import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import com.example.android.systemupdatersample.UpdateConfig;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.util.Arrays;
import java.util.List;
/**
* Tests for {@link UpdateConfigs}
*/
@RunWith(AndroidJUnit4.class)
@SmallTest
public class UpdateConfigsTest {
private Context mContext;
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
}
@Test
public void configsToNames_extractsNames() {
List<UpdateConfig> configs = Arrays.asList(
new UpdateConfig("blah", "http://", UpdateConfig.TYPE_NON_STREAMING),
new UpdateConfig("blah 2", "http://", UpdateConfig.TYPE_STREAMING)
);
String[] names = UpdateConfigs.configsToNames(configs);
assertArrayEquals(new String[] {"blah", "blah 2"}, names);
}
}
+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Given a OTA package file, produces update config JSON file.
Example: tools/gen_update.config.py \\
--ab_install_type=STREAMING \\
ota-build-001.zip \\
my-config-001.json \\
http://foo.bar/ota-builds/ota-build-001.zip
"""
import argparse
import json
import os.path
import sys
import zipfile
class GenUpdateConfig(object):
"""
A class that generates update configuration file from an OTA package.
Currently supports only A/B (seamless) OTA packages.
TODO: add non-A/B packages support.
"""
AB_INSTALL_TYPE_STREAMING = 'STREAMING'
AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
METADATA_NAME = 'META-INF/com/android/metadata'
def __init__(self, package, url, ab_install_type):
self.package = package
self.url = url
self.ab_install_type = ab_install_type
self.streaming_required = (
# payload.bin and payload_properties.txt must exist.
'payload.bin',
'payload_properties.txt',
)
self.streaming_optional = (
# care_map.txt is available only if dm-verity is enabled.
'care_map.txt',
# compatibility.zip is available only if target supports Treble.
'compatibility.zip',
)
self._config = None
@property
def config(self):
"""Returns generated config object."""
return self._config
def run(self):
"""Generates config."""
streaming_metadata = None
if self.ab_install_type == GenUpdateConfig.AB_INSTALL_TYPE_STREAMING:
streaming_metadata = self._gen_ab_streaming_metadata()
self._config = {
'__': '*** Generated using tools/gen_update_config.py ***',
'name': self.ab_install_type[0] + ' ' + os.path.basename(self.package)[:-4],
'url': self.url,
'ab_streaming_metadata': streaming_metadata,
'ab_install_type': self.ab_install_type,
}
def _gen_ab_streaming_metadata(self):
"""Builds metadata for files required for streaming update."""
with zipfile.ZipFile(self.package, 'r') as package_zip:
property_files = self._get_property_files(package_zip)
metadata = {
'property_files': property_files
}
return metadata
def _get_property_files(self, zip_file):
"""Constructs the property-files list for A/B streaming metadata."""
def compute_entry_offset_size(name):
"""Computes the zip entry offset and size."""
info = zip_file.getinfo(name)
offset = info.header_offset + len(info.FileHeader())
size = info.file_size
return {
'filename': os.path.basename(name),
'offset': offset,
'size': size,
}
property_files = []
for entry in self.streaming_required:
property_files.append(compute_entry_offset_size(entry))
for entry in self.streaming_optional:
if entry in zip_file.namelist():
property_files.append(compute_entry_offset_size(entry))
# 'META-INF/com/android/metadata' is required
property_files.append(compute_entry_offset_size(GenUpdateConfig.METADATA_NAME))
return property_files
def write(self, out):
"""Writes config to the output file."""
with open(out, 'w') as out_file:
json.dump(self.config, out_file, indent=4, separators=(',', ': '), sort_keys=True)
def main(): # pylint: disable=missing-docstring
ab_install_type_choices = [
GenUpdateConfig.AB_INSTALL_TYPE_STREAMING,
GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING]
parser = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument('--ab_install_type',
type=str,
default=GenUpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
choices=ab_install_type_choices,
help='A/B update installation type')
parser.add_argument('package',
type=str,
help='OTA package zip file')
parser.add_argument('out',
type=str,
help='Update configuration JSON file')
parser.add_argument('url',
type=str,
help='OTA package download url')
args = parser.parse_args()
if not args.out.endswith('.json'):
print('out must be a json file')
sys.exit(1)
gen = GenUpdateConfig(
package=args.package,
url=args.url,
ab_install_type=args.ab_install_type)
gen.run()
gen.write(args.out)
print('Config is written to ' + args.out)
if __name__ == '__main__':
main()
+55
View File
@@ -0,0 +1,55 @@
#!/usr/bin/env python3
#
# Copyright (C) 2018 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Tests gen_update_config.py
"""
import os.path
import unittest
from gen_update_config import GenUpdateConfig
class GenUpdateConfigTest(unittest.TestCase): # pylint: disable=missing-docstring
def test_ab_install_type_streaming(self):
"""tests if streaming property files' offset and size are generated properly"""
config, package = self._generate_config()
property_files = config['ab_streaming_metadata']['property_files']
self.assertEqual(len(property_files), 5)
with open(package, 'rb') as pkg_file:
for prop in property_files:
filename, offset, size = prop['filename'], prop['offset'], prop['size']
pkg_file.seek(offset)
data = pkg_file.read(size).decode('ascii')
# data in the archive are just uppercase filenames without extension
expected_data = filename.split('.')[0].upper()
self.assertEqual(data, expected_data)
@staticmethod
def _generate_config():
"""Generates JSON config from ota_002_package.zip."""
ota_package = os.path.join(os.path.dirname(__file__),
'../tests/res/raw/ota_002_package.zip')
gen = GenUpdateConfig(ota_package,
'file:///foo.bar',
GenUpdateConfig.AB_INSTALL_TYPE_STREAMING)
gen.run()
return gen.config, ota_package
if __name__ == '__main__':
unittest.main()