Snap for 4762932 from 96106592a2 to qt-release
Change-Id: I003a1ed8fe9bbb969efec6105556c5adc38f79a8
This commit is contained in:
@@ -126,6 +126,7 @@ LOCAL_SRC_FILES := \
|
||||
device.cpp \
|
||||
fuse_sdcard_provider.cpp \
|
||||
recovery.cpp \
|
||||
recovery_main.cpp \
|
||||
roots.cpp \
|
||||
rotate_logs.cpp \
|
||||
|
||||
|
||||
@@ -37,9 +37,13 @@ extern std::string stage;
|
||||
// The reason argument provided in "--reason=".
|
||||
extern const char* reason;
|
||||
|
||||
// fopen a file, mounting volumes and making parent dirs as necessary.
|
||||
// fopen(3)'s the given file, by mounting volumes and making parent dirs as necessary. Returns the
|
||||
// file pointer, or nullptr on error.
|
||||
FILE* fopen_path(const std::string& path, const char* mode);
|
||||
|
||||
// In turn fflush(3)'s, fsync(3)'s and fclose(3)'s the given stream.
|
||||
void check_and_fclose(FILE* fp, const std::string& name);
|
||||
|
||||
void ui_print(const char* format, ...) __printflike(1, 2);
|
||||
|
||||
bool is_ro_debuggable();
|
||||
|
||||
+18
-11
@@ -16,9 +16,13 @@
|
||||
|
||||
#include "device.h"
|
||||
|
||||
#include <android-base/logging.h>
|
||||
#include <android-base/macros.h>
|
||||
|
||||
#include "ui.h"
|
||||
|
||||
static const char* MENU_ITEMS[] = {
|
||||
// clang-format off
|
||||
static constexpr const char* kItems[]{
|
||||
"Reboot system now",
|
||||
"Reboot to bootloader",
|
||||
"Apply update from ADB",
|
||||
@@ -32,10 +36,11 @@ static const char* MENU_ITEMS[] = {
|
||||
"Run graphics test",
|
||||
"Run locale test",
|
||||
"Power off",
|
||||
nullptr,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static const Device::BuiltinAction MENU_ACTIONS[] = {
|
||||
// clang-format off
|
||||
static constexpr Device::BuiltinAction kMenuActions[] {
|
||||
Device::REBOOT,
|
||||
Device::REBOOT_BOOTLOADER,
|
||||
Device::APPLY_ADB_SIDELOAD,
|
||||
@@ -50,18 +55,20 @@ static const Device::BuiltinAction MENU_ACTIONS[] = {
|
||||
Device::RUN_LOCALE_TEST,
|
||||
Device::SHUTDOWN,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static_assert(sizeof(MENU_ITEMS) / sizeof(MENU_ITEMS[0]) ==
|
||||
sizeof(MENU_ACTIONS) / sizeof(MENU_ACTIONS[0]) + 1,
|
||||
"MENU_ITEMS and MENU_ACTIONS should have the same length, "
|
||||
"except for the extra NULL entry in MENU_ITEMS.");
|
||||
static_assert(arraysize(kItems) == arraysize(kMenuActions),
|
||||
"kItems and kMenuActions should have the same length.");
|
||||
|
||||
const char* const* Device::GetMenuItems() {
|
||||
return MENU_ITEMS;
|
||||
static const std::vector<std::string> kMenuItems(kItems, kItems + arraysize(kItems));
|
||||
|
||||
const std::vector<std::string>& Device::GetMenuItems() {
|
||||
return kMenuItems;
|
||||
}
|
||||
|
||||
Device::BuiltinAction Device::InvokeMenuItem(int menu_position) {
|
||||
return menu_position < 0 ? NO_ACTION : MENU_ACTIONS[menu_position];
|
||||
Device::BuiltinAction Device::InvokeMenuItem(size_t menu_position) {
|
||||
// CHECK_LT(menu_position, );
|
||||
return kMenuActions[menu_position];
|
||||
}
|
||||
|
||||
int Device::HandleMenuKey(int key, bool visible) {
|
||||
|
||||
@@ -17,11 +17,37 @@
|
||||
#ifndef _RECOVERY_DEVICE_H
|
||||
#define _RECOVERY_DEVICE_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Forward declaration to avoid including "ui.h".
|
||||
class RecoveryUI;
|
||||
|
||||
class Device {
|
||||
public:
|
||||
static constexpr const int kNoAction = -1;
|
||||
static constexpr const int kHighlightUp = -2;
|
||||
static constexpr const int kHighlightDown = -3;
|
||||
static constexpr const int kInvokeItem = -4;
|
||||
|
||||
enum BuiltinAction {
|
||||
NO_ACTION = 0,
|
||||
REBOOT = 1,
|
||||
APPLY_SDCARD = 2,
|
||||
// APPLY_CACHE was 3.
|
||||
APPLY_ADB_SIDELOAD = 4,
|
||||
WIPE_DATA = 5,
|
||||
WIPE_CACHE = 6,
|
||||
REBOOT_BOOTLOADER = 7,
|
||||
SHUTDOWN = 8,
|
||||
VIEW_RECOVERY_LOGS = 9,
|
||||
MOUNT_SYSTEM = 10,
|
||||
RUN_GRAPHICS_TEST = 11,
|
||||
RUN_LOCALE_TEST = 12,
|
||||
};
|
||||
|
||||
explicit Device(RecoveryUI* ui) : ui_(ui) {}
|
||||
virtual ~Device() {}
|
||||
|
||||
@@ -48,44 +74,23 @@ class Device {
|
||||
//
|
||||
// Returns one of the defined constants below in order to:
|
||||
//
|
||||
// - move the menu highlight (kHighlight{Up,Down})
|
||||
// - invoke the highlighted item (kInvokeItem)
|
||||
// - do nothing (kNoAction)
|
||||
// - invoke a specific action (a menu position: any non-negative number)
|
||||
// - move the menu highlight (kHighlight{Up,Down}: negative value)
|
||||
// - invoke the highlighted item (kInvokeItem: negative value)
|
||||
// - do nothing (kNoAction: negative value)
|
||||
// - invoke a specific action (a menu position: non-negative value)
|
||||
virtual int HandleMenuKey(int key, bool visible);
|
||||
|
||||
enum BuiltinAction {
|
||||
NO_ACTION = 0,
|
||||
REBOOT = 1,
|
||||
APPLY_SDCARD = 2,
|
||||
// APPLY_CACHE was 3.
|
||||
APPLY_ADB_SIDELOAD = 4,
|
||||
WIPE_DATA = 5,
|
||||
WIPE_CACHE = 6,
|
||||
REBOOT_BOOTLOADER = 7,
|
||||
SHUTDOWN = 8,
|
||||
VIEW_RECOVERY_LOGS = 9,
|
||||
MOUNT_SYSTEM = 10,
|
||||
RUN_GRAPHICS_TEST = 11,
|
||||
RUN_LOCALE_TEST = 12,
|
||||
};
|
||||
// Returns the list of menu items (a vector of strings). The menu_position passed to
|
||||
// InvokeMenuItem will correspond to the indexes into this array.
|
||||
virtual const std::vector<std::string>& GetMenuItems();
|
||||
|
||||
// Return the list of menu items (an array of strings, NULL-terminated). The menu_position passed
|
||||
// to InvokeMenuItem will correspond to the indexes into this array.
|
||||
virtual const char* const* GetMenuItems();
|
||||
|
||||
// Perform a recovery action selected from the menu. 'menu_position' will be the item number of
|
||||
// the selected menu item, or a non-negative number returned from HandleMenuKey(). The menu will
|
||||
// be hidden when this is called; implementations can call ui_print() to print information to the
|
||||
// Performs a recovery action selected from the menu. 'menu_position' will be the index of the
|
||||
// selected menu item, or a non-negative value returned from HandleMenuKey(). The menu will be
|
||||
// hidden when this is called; implementations can call ui_print() to print information to the
|
||||
// screen. If the menu position is one of the builtin actions, you can just return the
|
||||
// corresponding enum value. If it is an action specific to your device, you actually perform it
|
||||
// here and return NO_ACTION.
|
||||
virtual BuiltinAction InvokeMenuItem(int menu_position);
|
||||
|
||||
static const int kNoAction = -1;
|
||||
static const int kHighlightUp = -2;
|
||||
static const int kHighlightDown = -3;
|
||||
static const int kInvokeItem = -4;
|
||||
virtual BuiltinAction InvokeMenuItem(size_t menu_position);
|
||||
|
||||
// Called before and after we do a wipe data/factory reset operation, either via a reboot from the
|
||||
// main system with the --wipe_data flag, or when the user boots into recovery image manually and
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
int start_recovery(int argc, char** argv);
|
||||
+42
-221
@@ -14,6 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "private/recovery.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <errno.h>
|
||||
@@ -35,7 +37,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -64,7 +66,6 @@
|
||||
#include "fuse_sdcard_provider.h"
|
||||
#include "fuse_sideload.h"
|
||||
#include "install.h"
|
||||
#include "minadbd/minadbd.h"
|
||||
#include "minui/minui.h"
|
||||
#include "otautil/DirUtil.h"
|
||||
#include "otautil/error_code.h"
|
||||
@@ -147,7 +148,6 @@ struct selabel_handle* sehandle;
|
||||
* 7b. the user reboots (pulling the battery, etc) into the main system
|
||||
*/
|
||||
|
||||
// Open a given path, mounting partitions as necessary.
|
||||
FILE* fopen_path(const std::string& path, const char* mode) {
|
||||
if (ensure_path_mounted(path.c_str()) != 0) {
|
||||
LOG(ERROR) << "Can't mount " << path;
|
||||
@@ -162,8 +162,7 @@ FILE* fopen_path(const std::string& path, const char* 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 std::string& name) {
|
||||
void check_and_fclose(FILE* fp, const std::string& name) {
|
||||
fflush(fp);
|
||||
if (fsync(fileno(fp)) == -1) {
|
||||
PLOG(ERROR) << "Failed to fsync " << name;
|
||||
@@ -186,92 +185,6 @@ bool reboot(const std::string& command) {
|
||||
return android::base::SetProperty(ANDROID_RB_PROPERTY, cmd);
|
||||
}
|
||||
|
||||
static void redirect_stdio(const char* filename) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) == -1) {
|
||||
PLOG(ERROR) << "pipe failed";
|
||||
|
||||
// Fall back to traditional logging mode without timestamps.
|
||||
// If these fail, there's not really anywhere to complain...
|
||||
freopen(filename, "a", stdout); setbuf(stdout, NULL);
|
||||
freopen(filename, "a", stderr); setbuf(stderr, NULL);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
PLOG(ERROR) << "fork failed";
|
||||
|
||||
// Fall back to traditional logging mode without timestamps.
|
||||
// If these fail, there's not really anywhere to complain...
|
||||
freopen(filename, "a", stdout); setbuf(stdout, NULL);
|
||||
freopen(filename, "a", stderr); setbuf(stderr, NULL);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
/// Close the unused write end.
|
||||
close(pipefd[1]);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Child logger to actually write to the log file.
|
||||
FILE* log_fp = fopen(filename, "ae");
|
||||
if (log_fp == nullptr) {
|
||||
PLOG(ERROR) << "fopen \"" << filename << "\" failed";
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
FILE* pipe_fp = fdopen(pipefd[0], "r");
|
||||
if (pipe_fp == nullptr) {
|
||||
PLOG(ERROR) << "fdopen failed";
|
||||
check_and_fclose(log_fp, filename);
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
char* line = nullptr;
|
||||
size_t len = 0;
|
||||
while (getline(&line, &len, pipe_fp) != -1) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double duration = std::chrono::duration_cast<std::chrono::duration<double>>(
|
||||
now - start).count();
|
||||
if (line[0] == '\n') {
|
||||
fprintf(log_fp, "[%12.6lf]\n", duration);
|
||||
} else {
|
||||
fprintf(log_fp, "[%12.6lf] %s", duration, line);
|
||||
}
|
||||
fflush(log_fp);
|
||||
}
|
||||
|
||||
PLOG(ERROR) << "getline failed";
|
||||
|
||||
free(line);
|
||||
check_and_fclose(log_fp, filename);
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
} else {
|
||||
// Redirect stdout/stderr to the logger process.
|
||||
// Close the unused read end.
|
||||
close(pipefd[0]);
|
||||
|
||||
setbuf(stdout, nullptr);
|
||||
setbuf(stderr, nullptr);
|
||||
|
||||
if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
|
||||
PLOG(ERROR) << "dup2 stdout failed";
|
||||
}
|
||||
if (dup2(pipefd[1], STDERR_FILENO) == -1) {
|
||||
PLOG(ERROR) << "dup2 stderr failed";
|
||||
}
|
||||
|
||||
close(pipefd[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// command line args come from, in decreasing precedence:
|
||||
// - the actual command line
|
||||
// - the bootloader control block (one per line, after "recovery")
|
||||
@@ -583,57 +496,6 @@ static bool erase_volume(const char* volume) {
|
||||
return (result == 0);
|
||||
}
|
||||
|
||||
// Display a menu with the specified 'headers' and 'items'. Device specific HandleMenuKey() may
|
||||
// return a positive number beyond the given range. Caller sets 'menu_only' to true to ensure only
|
||||
// a menu item gets selected. 'initial_selection' controls the initial cursor location. Returns the
|
||||
// (non-negative) chosen item number, or -1 if timed out waiting for input.
|
||||
static int get_menu_selection(const char* const* headers, const char* const* items, bool menu_only,
|
||||
int initial_selection, Device* device) {
|
||||
// Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
|
||||
ui->FlushKeys();
|
||||
|
||||
ui->StartMenu(headers, items, initial_selection);
|
||||
|
||||
int selected = initial_selection;
|
||||
int chosen_item = -1;
|
||||
while (chosen_item < 0) {
|
||||
int key = ui->WaitKey();
|
||||
if (key == -1) { // WaitKey() timed out.
|
||||
if (ui->WasTextEverVisible()) {
|
||||
continue;
|
||||
} else {
|
||||
LOG(INFO) << "Timed out waiting for key input; rebooting.";
|
||||
ui->EndMenu();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
bool visible = ui->IsTextVisible();
|
||||
int action = device->HandleMenuKey(key, visible);
|
||||
|
||||
if (action < 0) {
|
||||
switch (action) {
|
||||
case Device::kHighlightUp:
|
||||
selected = ui->SelectMenu(--selected);
|
||||
break;
|
||||
case Device::kHighlightDown:
|
||||
selected = ui->SelectMenu(++selected);
|
||||
break;
|
||||
case Device::kInvokeItem:
|
||||
chosen_item = selected;
|
||||
break;
|
||||
case Device::kNoAction:
|
||||
break;
|
||||
}
|
||||
} else if (!menu_only) {
|
||||
chosen_item = action;
|
||||
}
|
||||
}
|
||||
|
||||
ui->EndMenu();
|
||||
return chosen_item;
|
||||
}
|
||||
|
||||
// Returns the selected filename, or an empty string.
|
||||
static std::string browse_directory(const std::string& path, Device* device) {
|
||||
ensure_path_mounted(path.c_str());
|
||||
@@ -645,7 +507,7 @@ static std::string browse_directory(const std::string& path, Device* device) {
|
||||
}
|
||||
|
||||
std::vector<std::string> dirs;
|
||||
std::vector<std::string> zips = { "../" }; // "../" is always the first entry.
|
||||
std::vector<std::string> entries{ "../" }; // "../" is always the first entry.
|
||||
|
||||
dirent* de;
|
||||
while ((de = readdir(d.get())) != nullptr) {
|
||||
@@ -656,29 +518,25 @@ static std::string browse_directory(const std::string& path, Device* device) {
|
||||
if (name == "." || name == "..") continue;
|
||||
dirs.push_back(name + "/");
|
||||
} else if (de->d_type == DT_REG && android::base::EndsWithIgnoreCase(name, ".zip")) {
|
||||
zips.push_back(name);
|
||||
entries.push_back(name);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(dirs.begin(), dirs.end());
|
||||
std::sort(zips.begin(), zips.end());
|
||||
std::sort(entries.begin(), entries.end());
|
||||
|
||||
// Append dirs to the zips list.
|
||||
zips.insert(zips.end(), dirs.begin(), dirs.end());
|
||||
// Append dirs to the entries list.
|
||||
entries.insert(entries.end(), dirs.begin(), dirs.end());
|
||||
|
||||
const char* entries[zips.size() + 1];
|
||||
entries[zips.size()] = nullptr;
|
||||
for (size_t i = 0; i < zips.size(); i++) {
|
||||
entries[i] = zips[i].c_str();
|
||||
}
|
||||
std::vector<std::string> headers{ "Choose a package to install:", path };
|
||||
|
||||
const char* headers[] = { "Choose a package to install:", path.c_str(), nullptr };
|
||||
|
||||
int chosen_item = 0;
|
||||
size_t chosen_item = 0;
|
||||
while (true) {
|
||||
chosen_item = get_menu_selection(headers, entries, true, chosen_item, device);
|
||||
chosen_item = ui->ShowMenu(
|
||||
headers, entries, chosen_item, true,
|
||||
std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
const std::string& item = zips[chosen_item];
|
||||
const std::string& item = entries[chosen_item];
|
||||
if (chosen_item == 0) {
|
||||
// Go up but continue browsing (if the caller is browse_directory).
|
||||
return "";
|
||||
@@ -700,15 +558,17 @@ static std::string browse_directory(const std::string& path, Device* device) {
|
||||
}
|
||||
|
||||
static bool yes_no(Device* device, const char* question1, const char* question2) {
|
||||
const char* headers[] = { question1, question2, NULL };
|
||||
const char* items[] = { " No", " Yes", NULL };
|
||||
std::vector<std::string> headers{ question1, question2 };
|
||||
std::vector<std::string> items{ " No", " Yes" };
|
||||
|
||||
int chosen_item = get_menu_selection(headers, items, true, 0, device);
|
||||
return (chosen_item == 1);
|
||||
size_t chosen_item = ui->ShowMenu(
|
||||
headers, items, 0, true,
|
||||
std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
|
||||
return (chosen_item == 1);
|
||||
}
|
||||
|
||||
static bool ask_to_wipe_data(Device* device) {
|
||||
return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!");
|
||||
return yes_no(device, "Wipe all user data?", " THIS CAN NOT BE UNDONE!");
|
||||
}
|
||||
|
||||
// Return true on success.
|
||||
@@ -735,20 +595,22 @@ static bool wipe_data(Device* device) {
|
||||
|
||||
static bool prompt_and_wipe_data(Device* device) {
|
||||
// Use a single string and let ScreenRecoveryUI handles the wrapping.
|
||||
const char* const headers[] = {
|
||||
std::vector<std::string> headers{
|
||||
"Can't load Android system. Your data may be corrupt. "
|
||||
"If you continue to get this message, you may need to "
|
||||
"perform a factory data reset and erase all user data "
|
||||
"stored on this device.",
|
||||
nullptr
|
||||
};
|
||||
const char* const items[] = {
|
||||
// clang-format off
|
||||
std::vector<std::string> items {
|
||||
"Try again",
|
||||
"Factory data reset",
|
||||
NULL
|
||||
};
|
||||
// clang-format on
|
||||
for (;;) {
|
||||
int chosen_item = get_menu_selection(headers, items, true, 0, device);
|
||||
size_t chosen_item = ui->ShowMenu(
|
||||
headers, items, 0, true,
|
||||
std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
|
||||
if (chosen_item != 1) {
|
||||
return true; // Just reboot, no wipe; not a failure, user asked for it
|
||||
}
|
||||
@@ -938,19 +800,16 @@ static void choose_recovery_file(Device* device) {
|
||||
|
||||
entries.push_back("Back");
|
||||
|
||||
std::vector<const char*> menu_entries(entries.size());
|
||||
std::transform(entries.cbegin(), entries.cend(), menu_entries.begin(),
|
||||
[](const std::string& entry) { return entry.c_str(); });
|
||||
menu_entries.push_back(nullptr);
|
||||
std::vector<std::string> headers{ "Select file to view" };
|
||||
|
||||
const char* headers[] = { "Select file to view", nullptr };
|
||||
|
||||
int chosen_item = 0;
|
||||
size_t chosen_item = 0;
|
||||
while (true) {
|
||||
chosen_item = get_menu_selection(headers, menu_entries.data(), true, chosen_item, device);
|
||||
chosen_item = ui->ShowMenu(
|
||||
headers, entries, chosen_item, true,
|
||||
std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
|
||||
if (entries[chosen_item] == "Back") break;
|
||||
|
||||
ui->ShowFile(entries[chosen_item].c_str());
|
||||
ui->ShowFile(entries[chosen_item]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1093,12 +952,15 @@ static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
|
||||
}
|
||||
ui->SetProgressType(RecoveryUI::EMPTY);
|
||||
|
||||
int chosen_item = get_menu_selection(nullptr, device->GetMenuItems(), false, 0, device);
|
||||
size_t chosen_item = ui->ShowMenu(
|
||||
{}, device->GetMenuItems(), 0, false,
|
||||
std::bind(&Device::HandleMenuKey, device, std::placeholders::_1, std::placeholders::_2));
|
||||
|
||||
// Device-specific code may take some action here. It may return one of the core actions
|
||||
// handled in the switch statement below.
|
||||
Device::BuiltinAction chosen_action =
|
||||
(chosen_item == -1) ? Device::REBOOT : device->InvokeMenuItem(chosen_item);
|
||||
Device::BuiltinAction chosen_action = (chosen_item == static_cast<size_t>(-1))
|
||||
? Device::REBOOT
|
||||
: device->InvokeMenuItem(chosen_item);
|
||||
|
||||
bool should_wipe_cache = false;
|
||||
switch (chosen_action) {
|
||||
@@ -1218,18 +1080,6 @@ void ui_print(const char* format, ...) {
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr char log_characters[] = "VDIWEF";
|
||||
|
||||
void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
|
||||
const char* /* tag */, const char* /* file */, unsigned int /* line */,
|
||||
const char* message) {
|
||||
if (severity >= android::base::ERROR && ui != nullptr) {
|
||||
ui->Print("E:%s\n", message);
|
||||
} else {
|
||||
fprintf(stdout, "%c:%s\n", log_characters[severity], message);
|
||||
}
|
||||
}
|
||||
|
||||
static bool is_battery_ok(int* required_battery_level) {
|
||||
using android::hardware::health::V1_0::BatteryStatus;
|
||||
using android::hardware::health::V2_0::Result;
|
||||
@@ -1359,38 +1209,9 @@ static void log_failure_code(ErrorCode code, const std::string& update_package)
|
||||
LOG(INFO) << log_content;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// We don't have logcat yet under recovery; so we'll print error on screen and
|
||||
// log to stdout (which is redirected to recovery.log) as we used to do.
|
||||
android::base::InitLogging(argv, &UiLogger);
|
||||
|
||||
// Take last pmsg contents and rewrite it to the current pmsg session.
|
||||
static const char filter[] = "recovery/";
|
||||
// Do we need to rotate?
|
||||
bool doRotate = false;
|
||||
|
||||
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &doRotate);
|
||||
// Take action to refresh pmsg contents
|
||||
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &doRotate);
|
||||
|
||||
// If this binary is started with the single argument "--adbd",
|
||||
// instead of being the normal recovery binary, it turns into kind
|
||||
// of a stripped-down version of adbd that only supports the
|
||||
// 'sideload' command. Note this must be a real argument, not
|
||||
// anything in the command file or bootloader control block; the
|
||||
// only way recovery should be run with this argument is when it
|
||||
// starts a copy of itself from the apply_from_adb() function.
|
||||
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
|
||||
minadbd_main();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int start_recovery(int argc, char** argv) {
|
||||
time_t start = time(nullptr);
|
||||
|
||||
// redirect_stdio should be called only in non-sideload mode. Otherwise
|
||||
// we may have two logger instances with different timestamps.
|
||||
redirect_stdio(Paths::Get().temporary_log_file().c_str());
|
||||
|
||||
printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
|
||||
|
||||
load_volume_table();
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include <android-base/logging.h>
|
||||
#include <private/android_logger.h> /* private pmsg functions */
|
||||
|
||||
#include "common.h"
|
||||
#include "minadbd/minadbd.h"
|
||||
#include "otautil/paths.h"
|
||||
#include "private/recovery.h"
|
||||
#include "rotate_logs.h"
|
||||
#include "ui.h"
|
||||
|
||||
static void UiLogger(android::base::LogId /* id */, android::base::LogSeverity severity,
|
||||
const char* /* tag */, const char* /* file */, unsigned int /* line */,
|
||||
const char* message) {
|
||||
static constexpr char log_characters[] = "VDIWEF";
|
||||
if (severity >= android::base::ERROR && ui != nullptr) {
|
||||
ui->Print("E:%s\n", message);
|
||||
} else {
|
||||
fprintf(stdout, "%c:%s\n", log_characters[severity], message);
|
||||
}
|
||||
}
|
||||
|
||||
static void redirect_stdio(const char* filename) {
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) == -1) {
|
||||
PLOG(ERROR) << "pipe failed";
|
||||
|
||||
// Fall back to traditional logging mode without timestamps. If these fail, there's not really
|
||||
// anywhere to complain...
|
||||
freopen(filename, "a", stdout);
|
||||
setbuf(stdout, nullptr);
|
||||
freopen(filename, "a", stderr);
|
||||
setbuf(stderr, nullptr);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid == -1) {
|
||||
PLOG(ERROR) << "fork failed";
|
||||
|
||||
// Fall back to traditional logging mode without timestamps. If these fail, there's not really
|
||||
// anywhere to complain...
|
||||
freopen(filename, "a", stdout);
|
||||
setbuf(stdout, nullptr);
|
||||
freopen(filename, "a", stderr);
|
||||
setbuf(stderr, nullptr);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
/// Close the unused write end.
|
||||
close(pipefd[1]);
|
||||
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
// Child logger to actually write to the log file.
|
||||
FILE* log_fp = fopen(filename, "ae");
|
||||
if (log_fp == nullptr) {
|
||||
PLOG(ERROR) << "fopen \"" << filename << "\" failed";
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
FILE* pipe_fp = fdopen(pipefd[0], "r");
|
||||
if (pipe_fp == nullptr) {
|
||||
PLOG(ERROR) << "fdopen failed";
|
||||
check_and_fclose(log_fp, filename);
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
char* line = nullptr;
|
||||
size_t len = 0;
|
||||
while (getline(&line, &len, pipe_fp) != -1) {
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
double duration =
|
||||
std::chrono::duration_cast<std::chrono::duration<double>>(now - start).count();
|
||||
if (line[0] == '\n') {
|
||||
fprintf(log_fp, "[%12.6lf]\n", duration);
|
||||
} else {
|
||||
fprintf(log_fp, "[%12.6lf] %s", duration, line);
|
||||
}
|
||||
fflush(log_fp);
|
||||
}
|
||||
|
||||
PLOG(ERROR) << "getline failed";
|
||||
|
||||
free(line);
|
||||
check_and_fclose(log_fp, filename);
|
||||
close(pipefd[0]);
|
||||
_exit(EXIT_FAILURE);
|
||||
} else {
|
||||
// Redirect stdout/stderr to the logger process. Close the unused read end.
|
||||
close(pipefd[0]);
|
||||
|
||||
setbuf(stdout, nullptr);
|
||||
setbuf(stderr, nullptr);
|
||||
|
||||
if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
|
||||
PLOG(ERROR) << "dup2 stdout failed";
|
||||
}
|
||||
if (dup2(pipefd[1], STDERR_FILENO) == -1) {
|
||||
PLOG(ERROR) << "dup2 stderr failed";
|
||||
}
|
||||
|
||||
close(pipefd[1]);
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// We don't have logcat yet under recovery; so we'll print error on screen and log to stdout
|
||||
// (which is redirected to recovery.log) as we used to do.
|
||||
android::base::InitLogging(argv, &UiLogger);
|
||||
|
||||
// Take last pmsg contents and rewrite it to the current pmsg session.
|
||||
static constexpr const char filter[] = "recovery/";
|
||||
// Do we need to rotate?
|
||||
bool do_rotate = false;
|
||||
|
||||
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logbasename, &do_rotate);
|
||||
// Take action to refresh pmsg contents
|
||||
__android_log_pmsg_file_read(LOG_ID_SYSTEM, ANDROID_LOG_INFO, filter, logrotate, &do_rotate);
|
||||
|
||||
// If this binary is started with the single argument "--adbd", instead of being the normal
|
||||
// recovery binary, it turns into kind of a stripped-down version of adbd that only supports the
|
||||
// 'sideload' command. Note this must be a real argument, not anything in the command file or
|
||||
// bootloader control block; the only way recovery should be run with this argument is when it
|
||||
// starts a copy of itself from the apply_from_adb() function.
|
||||
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
|
||||
minadbd_main();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// redirect_stdio should be called only in non-sideload mode. Otherwise we may have two logger
|
||||
// instances with different timestamps.
|
||||
redirect_stdio(Paths::Get().temporary_log_file().c_str());
|
||||
|
||||
return start_recovery(argc, argv);
|
||||
}
|
||||
+106
-62
@@ -31,6 +31,7 @@
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
@@ -42,7 +43,6 @@
|
||||
#include <android-base/strings.h>
|
||||
#include <minui/minui.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "device.h"
|
||||
#include "ui.h"
|
||||
|
||||
@@ -53,17 +53,27 @@ static double now() {
|
||||
return tv.tv_sec + tv.tv_usec / 1000000.0;
|
||||
}
|
||||
|
||||
Menu::Menu(bool scrollable, size_t max_items, size_t max_length)
|
||||
Menu::Menu(bool scrollable, size_t max_items, size_t max_length,
|
||||
const std::vector<std::string>& headers, const std::vector<std::string>& items,
|
||||
size_t initial_selection)
|
||||
: scrollable_(scrollable),
|
||||
max_display_items_(max_items),
|
||||
max_item_length_(max_length),
|
||||
text_headers_(nullptr),
|
||||
text_headers_(headers),
|
||||
menu_start_(0),
|
||||
selection_(0) {
|
||||
selection_(initial_selection) {
|
||||
CHECK_LE(max_items, static_cast<size_t>(std::numeric_limits<int>::max()));
|
||||
|
||||
// It's fine to have more entries than text_rows_ if scrollable menu is supported.
|
||||
size_t items_count = scrollable_ ? items.size() : std::min(items.size(), max_display_items_);
|
||||
for (size_t i = 0; i < items_count; ++i) {
|
||||
text_items_.emplace_back(items[i].substr(0, max_item_length_));
|
||||
}
|
||||
|
||||
CHECK(!text_items_.empty());
|
||||
}
|
||||
|
||||
const char* const* Menu::text_headers() const {
|
||||
const std::vector<std::string>& Menu::text_headers() const {
|
||||
return text_headers_;
|
||||
}
|
||||
|
||||
@@ -86,28 +96,15 @@ size_t Menu::ItemsCount() const {
|
||||
}
|
||||
|
||||
bool Menu::ItemsOverflow(std::string* cur_selection_str) const {
|
||||
if (!scrollable_ || static_cast<size_t>(ItemsCount()) <= max_display_items_) {
|
||||
if (!scrollable_ || ItemsCount() <= max_display_items_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*cur_selection_str =
|
||||
android::base::StringPrintf("Current item: %d/%zu", selection_ + 1, ItemsCount());
|
||||
android::base::StringPrintf("Current item: %zu/%zu", selection_ + 1, ItemsCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
void Menu::Start(const char* const* headers, const char* const* items, int initial_selection) {
|
||||
text_headers_ = headers;
|
||||
|
||||
// It's fine to have more entries than text_rows_ if scrollable menu is supported.
|
||||
size_t max_items_count = scrollable_ ? std::numeric_limits<int>::max() : max_display_items_;
|
||||
for (size_t i = 0; i < max_items_count && items[i] != nullptr; ++i) {
|
||||
text_items_.emplace_back(items[i], strnlen(items[i], max_item_length_));
|
||||
}
|
||||
|
||||
CHECK(!text_items_.empty());
|
||||
selection_ = initial_selection;
|
||||
}
|
||||
|
||||
// TODO(xunchang) modify the function parameters to button up & down.
|
||||
int Menu::Select(int sel) {
|
||||
CHECK_LE(ItemsCount(), static_cast<size_t>(std::numeric_limits<int>::max()));
|
||||
@@ -373,19 +370,22 @@ void ScreenRecoveryUI::SelectAndShowBackgroundText(const std::vector<std::string
|
||||
// Write the header and descriptive texts.
|
||||
SetColor(INFO);
|
||||
std::string header = "Show background text image";
|
||||
text_y += DrawTextLine(text_x, text_y, header.c_str(), true);
|
||||
text_y += DrawTextLine(text_x, text_y, header, true);
|
||||
std::string locale_selection = android::base::StringPrintf(
|
||||
"Current locale: %s, %zu/%zu", locales_entries[sel].c_str(), sel, locales_entries.size());
|
||||
const char* instruction[] = { locale_selection.c_str(),
|
||||
"Use volume up/down to switch locales and power to exit.",
|
||||
nullptr };
|
||||
// clang-format off
|
||||
std::vector<std::string> instruction = {
|
||||
locale_selection,
|
||||
"Use volume up/down to switch locales and power to exit."
|
||||
};
|
||||
// clang-format on
|
||||
text_y += DrawWrappedTextLines(text_x, text_y, instruction);
|
||||
|
||||
// Iterate through the text images and display them in order for the current locale.
|
||||
for (const auto& p : surfaces) {
|
||||
text_y += line_spacing;
|
||||
SetColor(LOG);
|
||||
text_y += DrawTextLine(text_x, text_y, p.first.c_str(), false);
|
||||
text_y += DrawTextLine(text_x, text_y, p.first, false);
|
||||
gr_color(255, 255, 255, 255);
|
||||
gr_texticon(text_x, text_y, p.second.get());
|
||||
text_y += gr_get_height(p.second.get());
|
||||
@@ -452,24 +452,23 @@ void ScreenRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
|
||||
gr_texticon(x, y, surface);
|
||||
}
|
||||
|
||||
int ScreenRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
|
||||
gr_text(gr_sys_font(), x, y, line, bold);
|
||||
int ScreenRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
|
||||
gr_text(gr_sys_font(), x, y, line.c_str(), bold);
|
||||
return char_height_ + 4;
|
||||
}
|
||||
|
||||
int ScreenRecoveryUI::DrawTextLines(int x, int y, const char* const* lines) const {
|
||||
int ScreenRecoveryUI::DrawTextLines(int x, int y, const std::vector<std::string>& lines) const {
|
||||
int offset = 0;
|
||||
for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
|
||||
offset += DrawTextLine(x, y + offset, lines[i], false);
|
||||
for (const auto& line : lines) {
|
||||
offset += DrawTextLine(x, y + offset, line, false);
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const char* const* lines) const {
|
||||
int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y,
|
||||
const std::vector<std::string>& lines) const {
|
||||
int offset = 0;
|
||||
for (size_t i = 0; lines != nullptr && lines[i] != nullptr; ++i) {
|
||||
// The line will be wrapped if it exceeds text_cols_.
|
||||
std::string line(lines[i]);
|
||||
for (const auto& line : lines) {
|
||||
size_t next_start = 0;
|
||||
while (next_start < line.size()) {
|
||||
std::string sub = line.substr(next_start, text_cols_ + 1);
|
||||
@@ -479,7 +478,7 @@ int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const char* const* line
|
||||
// Line too long and must be wrapped to text_cols_ columns.
|
||||
size_t last_space = sub.find_last_of(" \t\n");
|
||||
if (last_space == std::string::npos) {
|
||||
// No space found, just draw as much as we can
|
||||
// No space found, just draw as much as we can.
|
||||
sub.resize(text_cols_);
|
||||
next_start += text_cols_;
|
||||
} else {
|
||||
@@ -487,23 +486,12 @@ int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y, const char* const* line
|
||||
next_start += last_space + 1;
|
||||
}
|
||||
}
|
||||
offset += DrawTextLine(x, y + offset, sub.c_str(), false);
|
||||
offset += DrawTextLine(x, y + offset, sub, false);
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
static const char* REGULAR_HELP[] = {
|
||||
"Use volume up/down and power.",
|
||||
nullptr,
|
||||
};
|
||||
|
||||
static const char* LONG_PRESS_HELP[] = {
|
||||
"Any button cycles highlight.",
|
||||
"Long-press activates.",
|
||||
nullptr,
|
||||
};
|
||||
|
||||
// Redraws everything on the screen. Does not flip pages. Should only be called with updateMutex
|
||||
// locked.
|
||||
void ScreenRecoveryUI::draw_screen_locked() {
|
||||
@@ -516,11 +504,21 @@ void ScreenRecoveryUI::draw_screen_locked() {
|
||||
gr_color(0, 0, 0, 255);
|
||||
gr_clear();
|
||||
|
||||
// clang-format off
|
||||
static std::vector<std::string> REGULAR_HELP{
|
||||
"Use volume up/down and power.",
|
||||
};
|
||||
static std::vector<std::string> LONG_PRESS_HELP{
|
||||
"Any button cycles highlight.",
|
||||
"Long-press activates.",
|
||||
};
|
||||
// clang-format on
|
||||
draw_menu_and_text_buffer_locked(HasThreeButtons() ? REGULAR_HELP : LONG_PRESS_HELP);
|
||||
}
|
||||
|
||||
// Draws the menu and text buffer on the screen. Should only be called with updateMutex locked.
|
||||
void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(const char* const* help_message) {
|
||||
void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(
|
||||
const std::vector<std::string>& help_message) {
|
||||
int y = kMarginHeight;
|
||||
if (menu_) {
|
||||
static constexpr int kMenuIndent = 4;
|
||||
@@ -531,7 +529,7 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(const char* const* help_
|
||||
std::string recovery_fingerprint =
|
||||
android::base::GetProperty("ro.bootimage.build.fingerprint", "");
|
||||
for (const auto& chunk : android::base::Split(recovery_fingerprint, ":")) {
|
||||
y += DrawTextLine(x, y, chunk.c_str(), false);
|
||||
y += DrawTextLine(x, y, chunk, false);
|
||||
}
|
||||
|
||||
y += DrawTextLines(x, y, help_message);
|
||||
@@ -546,7 +544,7 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(const char* const* help_
|
||||
// screen.
|
||||
std::string cur_selection_str;
|
||||
if (menu_->ItemsOverflow(&cur_selection_str)) {
|
||||
y += DrawTextLine(x, y, cur_selection_str.c_str(), true);
|
||||
y += DrawTextLine(x, y, cur_selection_str, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -570,7 +568,7 @@ void ScreenRecoveryUI::draw_menu_and_text_buffer_locked(const char* const* help_
|
||||
bold = true;
|
||||
}
|
||||
|
||||
y += DrawTextLine(x, y, menu_->TextItem(i).c_str(), bold);
|
||||
y += DrawTextLine(x, y, menu_->TextItem(i), bold);
|
||||
|
||||
SetColor(MENU);
|
||||
}
|
||||
@@ -951,10 +949,10 @@ void ScreenRecoveryUI::ShowFile(FILE* fp) {
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenRecoveryUI::ShowFile(const char* filename) {
|
||||
FILE* fp = fopen_path(filename, "re");
|
||||
if (fp == nullptr) {
|
||||
Print(" Unable to open %s: %s\n", filename, strerror(errno));
|
||||
void ScreenRecoveryUI::ShowFile(const std::string& filename) {
|
||||
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen(filename.c_str(), "re"), fclose);
|
||||
if (!fp) {
|
||||
Print(" Unable to open %s: %s\n", filename.c_str(), strerror(errno));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -966,21 +964,19 @@ void ScreenRecoveryUI::ShowFile(const char* filename) {
|
||||
text_ = file_viewer_text_;
|
||||
ClearText();
|
||||
|
||||
ShowFile(fp);
|
||||
fclose(fp);
|
||||
ShowFile(fp.get());
|
||||
|
||||
text_ = old_text;
|
||||
text_col_ = old_text_col;
|
||||
text_row_ = old_text_row;
|
||||
}
|
||||
|
||||
void ScreenRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
|
||||
int initial_selection) {
|
||||
void ScreenRecoveryUI::StartMenu(const std::vector<std::string>& headers,
|
||||
const std::vector<std::string>& items, size_t initial_selection) {
|
||||
pthread_mutex_lock(&updateMutex);
|
||||
if (text_rows_ > 0 && text_cols_ > 1) {
|
||||
menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1);
|
||||
menu_->Start(headers, items, initial_selection);
|
||||
|
||||
menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_, text_cols_ - 1, headers, items,
|
||||
initial_selection);
|
||||
update_screen_locked();
|
||||
}
|
||||
pthread_mutex_unlock(&updateMutex);
|
||||
@@ -1009,6 +1005,54 @@ void ScreenRecoveryUI::EndMenu() {
|
||||
pthread_mutex_unlock(&updateMutex);
|
||||
}
|
||||
|
||||
size_t ScreenRecoveryUI::ShowMenu(const std::vector<std::string>& headers,
|
||||
const std::vector<std::string>& items, size_t initial_selection,
|
||||
bool menu_only,
|
||||
const std::function<int(int, bool)>& key_handler) {
|
||||
// Throw away keys pressed previously, so user doesn't accidentally trigger menu items.
|
||||
FlushKeys();
|
||||
|
||||
StartMenu(headers, items, initial_selection);
|
||||
|
||||
int selected = initial_selection;
|
||||
int chosen_item = -1;
|
||||
while (chosen_item < 0) {
|
||||
int key = WaitKey();
|
||||
if (key == -1) { // WaitKey() timed out.
|
||||
if (WasTextEverVisible()) {
|
||||
continue;
|
||||
} else {
|
||||
LOG(INFO) << "Timed out waiting for key input; rebooting.";
|
||||
EndMenu();
|
||||
return static_cast<size_t>(-1);
|
||||
}
|
||||
}
|
||||
|
||||
bool visible = IsTextVisible();
|
||||
int action = key_handler(key, visible);
|
||||
if (action < 0) {
|
||||
switch (action) {
|
||||
case Device::kHighlightUp:
|
||||
selected = SelectMenu(--selected);
|
||||
break;
|
||||
case Device::kHighlightDown:
|
||||
selected = SelectMenu(++selected);
|
||||
break;
|
||||
case Device::kInvokeItem:
|
||||
chosen_item = selected;
|
||||
break;
|
||||
case Device::kNoAction:
|
||||
break;
|
||||
}
|
||||
} else if (!menu_only) {
|
||||
chosen_item = action;
|
||||
}
|
||||
}
|
||||
|
||||
EndMenu();
|
||||
return chosen_item;
|
||||
}
|
||||
|
||||
bool ScreenRecoveryUI::IsTextVisible() {
|
||||
pthread_mutex_lock(&updateMutex);
|
||||
int visible = show_text;
|
||||
|
||||
+34
-21
@@ -20,6 +20,7 @@
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -32,20 +33,26 @@ struct GRSurface;
|
||||
// This class maintains the menu selection and display of the screen ui.
|
||||
class Menu {
|
||||
public:
|
||||
Menu(bool scrollable, size_t max_items, size_t max_length);
|
||||
// Constructs a Menu instance with the given |headers|, |items| and properties. Sets the initial
|
||||
// selection to |initial_selection|.
|
||||
Menu(bool scrollable, size_t max_items, size_t max_length,
|
||||
const std::vector<std::string>& headers, const std::vector<std::string>& items,
|
||||
size_t initial_selection);
|
||||
|
||||
bool scrollable() const {
|
||||
return scrollable_;
|
||||
}
|
||||
|
||||
int selection() const {
|
||||
size_t selection() const {
|
||||
return selection_;
|
||||
}
|
||||
|
||||
// Returns count of menu items.
|
||||
size_t ItemsCount() const;
|
||||
|
||||
// Returns the index of the first menu item.
|
||||
size_t MenuStart() const;
|
||||
|
||||
// Returns the index of the last menu item + 1.
|
||||
size_t MenuEnd() const;
|
||||
|
||||
@@ -60,17 +67,13 @@ class Menu {
|
||||
// /cache/recovery/last_log.1
|
||||
// /cache/recovery/last_log.2
|
||||
// ...
|
||||
const char* const* text_headers() const;
|
||||
const std::vector<std::string>& text_headers() const;
|
||||
std::string TextItem(size_t index) const;
|
||||
|
||||
// Checks if the menu items fit vertically on the screen. Returns true and set the
|
||||
// |cur_selection_str| if the items exceed the screen limit.
|
||||
bool ItemsOverflow(std::string* cur_selection_str) const;
|
||||
|
||||
// Starts the menu with |headers| and |items| in text. Sets the default selection to
|
||||
// |initial_selection|.
|
||||
void Start(const char* const* headers, const char* const* items, int initial_selection);
|
||||
|
||||
// Sets the current selection to |sel|. Handle the overflow cases depending on if the menu is
|
||||
// scrollable.
|
||||
int Select(int sel);
|
||||
@@ -82,15 +85,14 @@ class Menu {
|
||||
const size_t max_display_items_;
|
||||
// The length of each item to fit horizontally on a screen.
|
||||
const size_t max_item_length_;
|
||||
|
||||
// Internal storage for the menu headers and items in text.
|
||||
const char* const* text_headers_;
|
||||
// The menu headers.
|
||||
std::vector<std::string> text_headers_;
|
||||
// The actual menu items trimmed to fit the given properties.
|
||||
std::vector<std::string> text_items_;
|
||||
|
||||
// The first item to display on the screen.
|
||||
size_t menu_start_;
|
||||
// Current menu selection.
|
||||
int selection_;
|
||||
size_t selection_;
|
||||
};
|
||||
|
||||
// Implementation of RecoveryUI appropriate for devices with a screen
|
||||
@@ -132,13 +134,12 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
// printing messages
|
||||
void Print(const char* fmt, ...) override __printflike(2, 3);
|
||||
void PrintOnScreenOnly(const char* fmt, ...) override __printflike(2, 3);
|
||||
void ShowFile(const char* filename) override;
|
||||
void ShowFile(const std::string& filename) override;
|
||||
|
||||
// menu display
|
||||
void StartMenu(const char* const* headers, const char* const* items,
|
||||
int initial_selection) override;
|
||||
int SelectMenu(int sel) override;
|
||||
void EndMenu() override;
|
||||
size_t ShowMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
|
||||
size_t initial_selection, bool menu_only,
|
||||
const std::function<int(int, bool)>& key_handler) override;
|
||||
|
||||
void KeyLongPress(int) override;
|
||||
|
||||
@@ -164,10 +165,22 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
|
||||
virtual bool InitTextParams();
|
||||
|
||||
// Displays some header text followed by a menu of items, which appears at the top of the screen
|
||||
// (in place of any scrolling ui_print() output, if necessary).
|
||||
virtual void StartMenu(const std::vector<std::string>& headers,
|
||||
const std::vector<std::string>& items, size_t initial_selection);
|
||||
|
||||
// Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
|
||||
// selected.
|
||||
virtual int SelectMenu(int sel);
|
||||
|
||||
// Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed.
|
||||
virtual void EndMenu();
|
||||
|
||||
virtual void draw_background_locked();
|
||||
virtual void draw_foreground_locked();
|
||||
virtual void draw_screen_locked();
|
||||
virtual void draw_menu_and_text_buffer_locked(const char* const* help_message);
|
||||
virtual void draw_menu_and_text_buffer_locked(const std::vector<std::string>& help_message);
|
||||
virtual void update_screen_locked();
|
||||
virtual void update_progress_locked();
|
||||
|
||||
@@ -201,7 +214,7 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
// Draws a horizontal rule at Y. Returns the offset it should be moving along Y-axis.
|
||||
virtual int DrawHorizontalRule(int y) const;
|
||||
// Draws a line of text. Returns the offset it should be moving along Y-axis.
|
||||
virtual int DrawTextLine(int x, int y, const char* line, bool bold) const;
|
||||
virtual int DrawTextLine(int x, int y, const std::string& line, bool bold) const;
|
||||
// Draws surface portion (sx, sy, w, h) at screen location (dx, dy).
|
||||
virtual void DrawSurface(GRSurface* surface, int sx, int sy, int w, int h, int dx, int dy) const;
|
||||
// Draws rectangle at (x, y) - (x + w, y + h).
|
||||
@@ -209,10 +222,10 @@ class ScreenRecoveryUI : public RecoveryUI {
|
||||
// Draws given surface (surface->pixel_bytes = 1) as text at (x, y).
|
||||
virtual void DrawTextIcon(int x, int y, GRSurface* surface) const;
|
||||
// Draws multiple text lines. Returns the offset it should be moving along Y-axis.
|
||||
int DrawTextLines(int x, int y, const char* const* lines) const;
|
||||
int DrawTextLines(int x, int y, const std::vector<std::string>& lines) const;
|
||||
// Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines.
|
||||
// Returns the offset it should be moving along Y-axis.
|
||||
int DrawWrappedTextLines(int x, int y, const char* const* lines) const;
|
||||
int DrawWrappedTextLines(int x, int y, const std::vector<std::string>& lines) const;
|
||||
|
||||
Icon currentIcon;
|
||||
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
#ifndef RECOVERY_STUB_UI_H
|
||||
#define RECOVERY_STUB_UI_H
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "ui.h"
|
||||
|
||||
// Stub implementation of RecoveryUI for devices without screen.
|
||||
@@ -51,15 +55,15 @@ class StubRecoveryUI : public RecoveryUI {
|
||||
va_end(ap);
|
||||
}
|
||||
void PrintOnScreenOnly(const char* /* fmt */, ...) override {}
|
||||
void ShowFile(const char* /* filename */) override {}
|
||||
void ShowFile(const std::string& /* filename */) override {}
|
||||
|
||||
// menu display
|
||||
void StartMenu(const char* const* /* headers */, const char* const* /* items */,
|
||||
int /* initial_selection */) override {}
|
||||
int SelectMenu(int sel) override {
|
||||
return sel;
|
||||
size_t ShowMenu(const std::vector<std::string>& /* headers */,
|
||||
const std::vector<std::string>& /* items */, size_t initial_selection,
|
||||
bool /* menu_only */,
|
||||
const std::function<int(int, bool)>& /* key_handler */) override {
|
||||
return initial_selection;
|
||||
}
|
||||
void EndMenu() override {}
|
||||
};
|
||||
|
||||
#endif // RECOVERY_STUB_UI_H
|
||||
|
||||
@@ -14,21 +14,22 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include "screen_ui.h"
|
||||
#include <stddef.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
constexpr const char* HEADER[] = { "header", nullptr };
|
||||
constexpr const char* ITEMS[] = { "items1", "items2", "items3", "items4", "1234567890", nullptr };
|
||||
#include "screen_ui.h"
|
||||
|
||||
static const std::vector<std::string> HEADERS{ "header" };
|
||||
static const std::vector<std::string> ITEMS{ "item1", "item2", "item3", "item4", "1234567890" };
|
||||
|
||||
TEST(ScreenUITest, StartPhoneMenuSmoke) {
|
||||
Menu menu(false, 10, 20);
|
||||
Menu menu(false, 10, 20, HEADERS, ITEMS, 0);
|
||||
ASSERT_FALSE(menu.scrollable());
|
||||
|
||||
menu.Start(HEADER, ITEMS, 0);
|
||||
ASSERT_EQ(HEADER[0], menu.text_headers()[0]);
|
||||
ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
|
||||
ASSERT_EQ(5u, menu.ItemsCount());
|
||||
|
||||
std::string message;
|
||||
@@ -41,11 +42,9 @@ TEST(ScreenUITest, StartPhoneMenuSmoke) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, StartWearMenuSmoke) {
|
||||
Menu menu(true, 10, 8);
|
||||
Menu menu(true, 10, 8, HEADERS, ITEMS, 1);
|
||||
ASSERT_TRUE(menu.scrollable());
|
||||
|
||||
menu.Start(HEADER, ITEMS, 1);
|
||||
ASSERT_EQ(HEADER[0], menu.text_headers()[0]);
|
||||
ASSERT_EQ(HEADERS[0], menu.text_headers()[0]);
|
||||
ASSERT_EQ(5u, menu.ItemsCount());
|
||||
|
||||
std::string message;
|
||||
@@ -59,10 +58,8 @@ TEST(ScreenUITest, StartWearMenuSmoke) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
|
||||
Menu menu(false, 1, 20);
|
||||
Menu menu(false, 1, 20, HEADERS, ITEMS, 0);
|
||||
ASSERT_FALSE(menu.scrollable());
|
||||
|
||||
menu.Start(HEADER, ITEMS, 0);
|
||||
ASSERT_EQ(1u, menu.ItemsCount());
|
||||
|
||||
std::string message;
|
||||
@@ -76,10 +73,8 @@ TEST(ScreenUITest, StartPhoneMenuItemsOverflow) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, StartWearMenuItemsOverflow) {
|
||||
Menu menu(true, 1, 20);
|
||||
Menu menu(true, 1, 20, HEADERS, ITEMS, 0);
|
||||
ASSERT_TRUE(menu.scrollable());
|
||||
|
||||
menu.Start(HEADER, ITEMS, 0);
|
||||
ASSERT_EQ(5u, menu.ItemsCount());
|
||||
|
||||
std::string message;
|
||||
@@ -95,10 +90,8 @@ TEST(ScreenUITest, StartWearMenuItemsOverflow) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, PhoneMenuSelectSmoke) {
|
||||
Menu menu(false, 10, 20);
|
||||
|
||||
int sel = 0;
|
||||
menu.Start(HEADER, ITEMS, sel);
|
||||
Menu menu(false, 10, 20, HEADERS, ITEMS, sel);
|
||||
// Mimic down button 10 times (2 * items size)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sel = menu.Select(++sel);
|
||||
@@ -126,10 +119,8 @@ TEST(ScreenUITest, PhoneMenuSelectSmoke) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, WearMenuSelectSmoke) {
|
||||
Menu menu(true, 10, 20);
|
||||
|
||||
int sel = 0;
|
||||
menu.Start(HEADER, ITEMS, sel);
|
||||
Menu menu(true, 10, 20, HEADERS, ITEMS, sel);
|
||||
// Mimic pressing down button 10 times (2 * items size)
|
||||
for (int i = 0; i < 10; i++) {
|
||||
sel = menu.Select(++sel);
|
||||
@@ -157,10 +148,8 @@ TEST(ScreenUITest, WearMenuSelectSmoke) {
|
||||
}
|
||||
|
||||
TEST(ScreenUITest, WearMenuSelectItemsOverflow) {
|
||||
Menu menu(true, 3, 20);
|
||||
|
||||
int sel = 1;
|
||||
menu.Start(HEADER, ITEMS, sel);
|
||||
Menu menu(true, 3, 20, HEADERS, ITEMS, sel);
|
||||
ASSERT_EQ(5u, menu.ItemsCount());
|
||||
|
||||
// Scroll the menu to the end, and check the start & end of menu.
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
#include <pthread.h>
|
||||
#include <time.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Abstract class for controlling the user interface during recovery.
|
||||
class RecoveryUI {
|
||||
@@ -87,7 +89,9 @@ class RecoveryUI {
|
||||
virtual void Print(const char* fmt, ...) __printflike(2, 3) = 0;
|
||||
virtual void PrintOnScreenOnly(const char* fmt, ...) __printflike(2, 3) = 0;
|
||||
|
||||
virtual void ShowFile(const char* filename) = 0;
|
||||
// Shows the contents of the given file. Caller ensures the patition that contains the file has
|
||||
// been mounted.
|
||||
virtual void ShowFile(const std::string& filename) = 0;
|
||||
|
||||
// --- key handling ---
|
||||
|
||||
@@ -128,17 +132,19 @@ class RecoveryUI {
|
||||
|
||||
// --- menu display ---
|
||||
|
||||
// Display some header text followed by a menu of items, which appears at the top of the screen
|
||||
// (in place of any scrolling ui_print() output, if necessary).
|
||||
virtual void StartMenu(const char* const* headers, const char* const* items,
|
||||
int initial_selection) = 0;
|
||||
|
||||
// Sets the menu highlight to the given index, wrapping if necessary. Returns the actual item
|
||||
// selected.
|
||||
virtual int SelectMenu(int sel) = 0;
|
||||
|
||||
// Ends menu mode, resetting the text overlay so that ui_print() statements will be displayed.
|
||||
virtual void EndMenu() = 0;
|
||||
// Displays a menu with the given 'headers' and 'items'. The supplied 'key_handler' callback,
|
||||
// which is typically bound to Device::HandleMenuKey(), should return the expected action for the
|
||||
// given key code and menu visibility (e.g. to move the cursor or to select an item). Caller sets
|
||||
// 'menu_only' to true to ensure only a menu item gets selected and returned. Otherwise if
|
||||
// 'menu_only' is false, ShowMenu() will forward any non-negative value returned from the
|
||||
// key_handler, which may be beyond the range of menu items. This could be used to trigger a
|
||||
// device-specific action, even without that being listed in the menu. Caller needs to handle
|
||||
// such a case accordingly (e.g. by calling Device::InvokeMenuItem() to process the action).
|
||||
// Returns a non-negative value (the chosen item number or device-specific action code), or
|
||||
// static_cast<size_t>(-1) if timed out waiting for input.
|
||||
virtual size_t ShowMenu(const std::vector<std::string>& headers,
|
||||
const std::vector<std::string>& items, size_t initial_selection,
|
||||
bool menu_only, const std::function<int(int, bool)>& key_handler) = 0;
|
||||
|
||||
protected:
|
||||
void EnqueueKey(int key_code);
|
||||
|
||||
@@ -26,6 +26,10 @@ LOCAL_PROGUARD_ENABLED := disabled
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES += guava
|
||||
|
||||
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
# Use the following include to make our test apk.
|
||||
|
||||
+45
-15
@@ -30,13 +30,19 @@ 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.
|
||||
SystemUpdaterSample app downloads OTA package from `url`. In this sample app
|
||||
`url` is expected to point to file system, e.g. `file:///data/sample-builds/ota-002.zip`.
|
||||
|
||||
If `ab_install_type` is `NON_STREAMING` then app checks if `url` starts
|
||||
with `file://` and passes `url` to the `update_engine`.
|
||||
|
||||
If `ab_install_type` is `STREAMING`, app downloads only the entries in need, as
|
||||
opposed to the entire package, to initiate a streaming update. The `payload.bin`
|
||||
entry, which takes up the majority of the space in an OTA package, will be
|
||||
streamed by `update_engine` directly. The ZIP entries in such a package need to be
|
||||
saved uncompressed (`ZIP_STORED`), so that their data can be downloaded directly
|
||||
with the offset and length. As `payload.bin` itself is already in compressed
|
||||
format, the size penalty is marginal.
|
||||
|
||||
Config files can be generated using `tools/gen_update_config.py`.
|
||||
Running `./tools/gen_update_config.py --help` shows usage of the script.
|
||||
@@ -44,11 +50,15 @@ 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`.
|
||||
The commands expected to be run from `$ANDROID_BUILD_TOP` and for demo
|
||||
purpose only.
|
||||
|
||||
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.
|
||||
3. Change permissions on `/data/ota_package/` to `0777` on the device.
|
||||
4. Set SELinux mode to permissive. See instructions below.
|
||||
5. Add update config files.
|
||||
6. Push OTA packages to the device.
|
||||
|
||||
|
||||
## Development
|
||||
@@ -86,13 +96,33 @@ The commands expected to be run from `$ANDROID_BUILD_TOP`.
|
||||
```
|
||||
|
||||
|
||||
## Getting access to `update_engine` API and read/write access to `/data`
|
||||
## Accessing `android.os.UpdateEngine` API
|
||||
|
||||
Run adb shell as a root, and set SELinux mode to permissive (0):
|
||||
`android.os.UpdateEngine`` APIs are marked as `@SystemApi`, meaning only system apps can access them.
|
||||
|
||||
|
||||
## Getting read/write access to `/data/ota_package/`
|
||||
|
||||
Following must be included in `AndroidManifest.xml`:
|
||||
|
||||
```xml
|
||||
<uses-permission android:name="android.permission.ACCESS_CACHE_FILESYSTEM" />
|
||||
```
|
||||
|
||||
Note: access to cache filesystem is granted only to system apps.
|
||||
|
||||
|
||||
## Setting SELinux mode to permissive (0)
|
||||
|
||||
```txt
|
||||
$ adb root
|
||||
$ adb shell
|
||||
# setenforce 0
|
||||
# getenforce
|
||||
local$ adb root
|
||||
local$ adb shell
|
||||
android# setenforce 0
|
||||
android# getenforce
|
||||
```
|
||||
|
||||
|
||||
## License
|
||||
|
||||
SystemUpdaterSample app is released under
|
||||
[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0).
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Running update status:" />
|
||||
android:text="Update status:" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewStatus"
|
||||
@@ -124,6 +124,28 @@
|
||||
android:text="@string/unknown" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Update completion:" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textViewCompletion"
|
||||
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"
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"__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)",
|
||||
"__url": "https:// or file:// uri to update package (zip, xz, ...)",
|
||||
"__type": "NON_STREAMING (from a 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": {
|
||||
"url": "http://foo.bar/builds/ota-001.zip",
|
||||
"ab_install_type": "NON_STREAMING",
|
||||
"ab_streaming_metadata": {
|
||||
"__": "streaming_metadata is required only for streaming update",
|
||||
"__property_files": "name, offset and size of files",
|
||||
"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": "name of the file in package",
|
||||
"__offset": "defines beginning of the file in package",
|
||||
"__size": "size of the file in package",
|
||||
"filename": "payload.bin",
|
||||
"offset": 531,
|
||||
"size": 5012323
|
||||
|
||||
@@ -18,12 +18,15 @@ package com.example.android.systemupdatersample;
|
||||
|
||||
import android.os.UpdateEngine;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Payload that will be given to {@link UpdateEngine#applyPayload)}.
|
||||
*/
|
||||
public class PayloadSpec {
|
||||
public class PayloadSpec implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 41043L;
|
||||
|
||||
/**
|
||||
* Creates a payload spec {@link Builder}
|
||||
|
||||
@@ -19,6 +19,7 @@ package com.example.android.systemupdatersample;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -26,13 +27,13 @@ import java.io.File;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* UpdateConfig describes an update. It will be parsed from JSON, which is intended to
|
||||
* An update description. It will be parsed from JSON, which is intended to
|
||||
* be sent from server to the update app, but in this sample app it will be stored on the device.
|
||||
*/
|
||||
public class UpdateConfig implements Parcelable {
|
||||
|
||||
public static final int TYPE_NON_STREAMING = 0;
|
||||
public static final int TYPE_STREAMING = 1;
|
||||
public static final int AB_INSTALL_TYPE_NON_STREAMING = 0;
|
||||
public static final int AB_INSTALL_TYPE_STREAMING = 1;
|
||||
|
||||
public static final Parcelable.Creator<UpdateConfig> CREATOR =
|
||||
new Parcelable.Creator<UpdateConfig>() {
|
||||
@@ -54,18 +55,30 @@ public class UpdateConfig implements Parcelable {
|
||||
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"));
|
||||
switch (o.getString("ab_install_type")) {
|
||||
case AB_INSTALL_TYPE_NON_STREAMING_JSON:
|
||||
c.mAbInstallType = AB_INSTALL_TYPE_NON_STREAMING;
|
||||
break;
|
||||
case AB_INSTALL_TYPE_STREAMING_JSON:
|
||||
c.mAbInstallType = AB_INSTALL_TYPE_STREAMING;
|
||||
break;
|
||||
default:
|
||||
throw new JSONException("Invalid type, expected either "
|
||||
+ "NON_STREAMING or STREAMING, got " + o.getString("ab_install_type"));
|
||||
}
|
||||
if (o.has("metadata")) {
|
||||
c.mMetadata = new Metadata(
|
||||
o.getJSONObject("metadata").getInt("offset"),
|
||||
o.getJSONObject("metadata").getInt("size"));
|
||||
if (c.mAbInstallType == AB_INSTALL_TYPE_STREAMING) {
|
||||
JSONObject meta = o.getJSONObject("ab_streaming_metadata");
|
||||
JSONArray propertyFilesJson = meta.getJSONArray("property_files");
|
||||
InnerFile[] propertyFiles =
|
||||
new InnerFile[propertyFilesJson.length()];
|
||||
for (int i = 0; i < propertyFilesJson.length(); i++) {
|
||||
JSONObject p = propertyFilesJson.getJSONObject(i);
|
||||
propertyFiles[i] = new InnerFile(
|
||||
p.getString("filename"),
|
||||
p.getLong("offset"),
|
||||
p.getLong("size"));
|
||||
}
|
||||
c.mAbStreamingMetadata = new StreamingMetadata(propertyFiles);
|
||||
}
|
||||
c.mRawJson = json;
|
||||
return c;
|
||||
@@ -74,8 +87,8 @@ public class UpdateConfig implements Parcelable {
|
||||
/**
|
||||
* 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";
|
||||
private static final String AB_INSTALL_TYPE_NON_STREAMING_JSON = "NON_STREAMING";
|
||||
private static final String AB_INSTALL_TYPE_STREAMING_JSON = "STREAMING";
|
||||
|
||||
/** name will be visible on UI */
|
||||
private String mName;
|
||||
@@ -84,10 +97,10 @@ public class UpdateConfig implements Parcelable {
|
||||
private String mUrl;
|
||||
|
||||
/** non-streaming (first saves locally) OR streaming (on the fly) */
|
||||
private int mInstallType;
|
||||
private int mAbInstallType;
|
||||
|
||||
/** metadata is required only for streaming update */
|
||||
private Metadata mMetadata;
|
||||
private StreamingMetadata mAbStreamingMetadata;
|
||||
|
||||
private String mRawJson;
|
||||
|
||||
@@ -97,15 +110,15 @@ public class UpdateConfig implements Parcelable {
|
||||
protected UpdateConfig(Parcel in) {
|
||||
this.mName = in.readString();
|
||||
this.mUrl = in.readString();
|
||||
this.mInstallType = in.readInt();
|
||||
this.mMetadata = (Metadata) in.readSerializable();
|
||||
this.mAbInstallType = in.readInt();
|
||||
this.mAbStreamingMetadata = (StreamingMetadata) in.readSerializable();
|
||||
this.mRawJson = in.readString();
|
||||
}
|
||||
|
||||
public UpdateConfig(String name, String url, int installType) {
|
||||
this.mName = name;
|
||||
this.mUrl = url;
|
||||
this.mInstallType = installType;
|
||||
this.mAbInstallType = installType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
@@ -121,16 +134,18 @@ public class UpdateConfig implements Parcelable {
|
||||
}
|
||||
|
||||
public int getInstallType() {
|
||||
return mInstallType;
|
||||
return mAbInstallType;
|
||||
}
|
||||
|
||||
public StreamingMetadata getStreamingMetadata() {
|
||||
return mAbStreamingMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* "url" must be the file located on the device.
|
||||
*
|
||||
* @return File object for given url
|
||||
*/
|
||||
public File getUpdatePackageFile() {
|
||||
if (mInstallType != TYPE_NON_STREAMING) {
|
||||
if (mAbInstallType != AB_INSTALL_TYPE_NON_STREAMING) {
|
||||
throw new RuntimeException("Expected non-streaming install type");
|
||||
}
|
||||
if (!mUrl.startsWith("file://")) {
|
||||
@@ -148,29 +163,60 @@ public class UpdateConfig implements Parcelable {
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(mName);
|
||||
dest.writeString(mUrl);
|
||||
dest.writeInt(mInstallType);
|
||||
dest.writeSerializable(mMetadata);
|
||||
dest.writeInt(mAbInstallType);
|
||||
dest.writeSerializable(mAbStreamingMetadata);
|
||||
dest.writeString(mRawJson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata for STREAMING update
|
||||
* Metadata for streaming A/B update.
|
||||
*/
|
||||
public static class Metadata implements Serializable {
|
||||
public static class StreamingMetadata implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 31042L;
|
||||
|
||||
/** defines beginning of update data in archive */
|
||||
private InnerFile[] mPropertyFiles;
|
||||
|
||||
public StreamingMetadata() {
|
||||
mPropertyFiles = new InnerFile[0];
|
||||
}
|
||||
|
||||
public StreamingMetadata(InnerFile[] propertyFiles) {
|
||||
this.mPropertyFiles = propertyFiles;
|
||||
}
|
||||
|
||||
public InnerFile[] getPropertyFiles() {
|
||||
return mPropertyFiles;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of a file in an OTA package zip file.
|
||||
*/
|
||||
public static class InnerFile implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 31043L;
|
||||
|
||||
/** filename in an archive */
|
||||
private String mFilename;
|
||||
|
||||
/** defines beginning of update data in archive */
|
||||
private long mOffset;
|
||||
|
||||
/** size of the update data in archive */
|
||||
private long mSize;
|
||||
|
||||
public Metadata(long offset, long size) {
|
||||
public InnerFile(String filename, long offset, long size) {
|
||||
this.mFilename = filename;
|
||||
this.mOffset = offset;
|
||||
this.mSize = size;
|
||||
}
|
||||
|
||||
public String getFilename() {
|
||||
return mFilename;
|
||||
}
|
||||
|
||||
public long getOffset() {
|
||||
return mOffset;
|
||||
}
|
||||
@@ -178,6 +224,7 @@ public class UpdateConfig implements Parcelable {
|
||||
public long getSize() {
|
||||
return mSize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,13 +31,15 @@ import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.android.systemupdatersample.PayloadSpec;
|
||||
import com.example.android.systemupdatersample.R;
|
||||
import com.example.android.systemupdatersample.UpdateConfig;
|
||||
import com.example.android.systemupdatersample.updates.AbNonStreamingUpdate;
|
||||
import com.example.android.systemupdatersample.util.PayloadSpecs;
|
||||
import com.example.android.systemupdatersample.util.UpdateConfigs;
|
||||
import com.example.android.systemupdatersample.util.UpdateEngineErrorCodes;
|
||||
import com.example.android.systemupdatersample.util.UpdateEngineStatuses;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@@ -46,6 +48,8 @@ import java.util.concurrent.atomic.AtomicInteger;
|
||||
*/
|
||||
public class MainActivity extends Activity {
|
||||
|
||||
private static final String TAG = "MainActivity";
|
||||
|
||||
private TextView mTextViewBuild;
|
||||
private Spinner mSpinnerConfigs;
|
||||
private TextView mTextViewConfigsDirHint;
|
||||
@@ -55,17 +59,19 @@ public class MainActivity extends Activity {
|
||||
private Button mButtonReset;
|
||||
private ProgressBar mProgressBar;
|
||||
private TextView mTextViewStatus;
|
||||
private TextView mTextViewCompletion;
|
||||
|
||||
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();
|
||||
|
||||
private final UpdateEngine mUpdateEngine = new UpdateEngine();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -80,14 +86,14 @@ public class MainActivity extends Activity {
|
||||
this.mButtonReset = findViewById(R.id.buttonReset);
|
||||
this.mProgressBar = findViewById(R.id.progressBar);
|
||||
this.mTextViewStatus = findViewById(R.id.textViewStatus);
|
||||
|
||||
this.mUpdateEngine.bind(mUpdateEngineCallback);
|
||||
this.mTextViewCompletion = findViewById(R.id.textViewCompletion);
|
||||
|
||||
this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
|
||||
|
||||
uiReset();
|
||||
|
||||
loadUpdateConfigs();
|
||||
|
||||
this.mUpdateEngine.bind(mUpdateEngineCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -140,7 +146,6 @@ public class MainActivity extends Activity {
|
||||
.setMessage("Do you really want to cancel running update?")
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
|
||||
uiReset();
|
||||
stopRunningUpdate();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null).show();
|
||||
@@ -156,7 +161,6 @@ public class MainActivity extends Activity {
|
||||
+ " 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();
|
||||
@@ -178,6 +182,13 @@ public class MainActivity extends Activity {
|
||||
setUiStatus(status);
|
||||
Toast.makeText(this, "Update Status changed", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
if (status != UpdateEngine.UpdateStatusConstants.IDLE) {
|
||||
Log.d(TAG, "status changed, setting ui to updating mode");
|
||||
uiSetUpdating();
|
||||
} else {
|
||||
Log.d(TAG, "status changed, resetting ui");
|
||||
uiReset();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -188,15 +199,16 @@ public class MainActivity extends Activity {
|
||||
* values from {@link UpdateEngine.ErrorCodeConstants}.
|
||||
*/
|
||||
private void onPayloadApplicationComplete(int errorCode) {
|
||||
final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
|
||||
? "SUCCESS"
|
||||
: "FAILURE";
|
||||
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();
|
||||
setUiCompletion(errorCode);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -212,6 +224,7 @@ public class MainActivity extends Activity {
|
||||
mProgressBar.setEnabled(false);
|
||||
mProgressBar.setVisibility(ProgressBar.INVISIBLE);
|
||||
mTextViewStatus.setText(R.string.unknown);
|
||||
mTextViewCompletion.setText(R.string.unknown);
|
||||
}
|
||||
|
||||
/** sets ui updating mode */
|
||||
@@ -239,7 +252,18 @@ public class MainActivity extends Activity {
|
||||
*/
|
||||
private void setUiStatus(int status) {
|
||||
String statusText = UpdateEngineStatuses.getStatusText(status);
|
||||
mTextViewStatus.setText(statusText);
|
||||
mTextViewStatus.setText(statusText + "/" + status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param errorCode update engine error code
|
||||
*/
|
||||
private void setUiCompletion(int errorCode) {
|
||||
final String state = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
|
||||
? "SUCCESS"
|
||||
: "FAILURE";
|
||||
String errorText = UpdateEngineErrorCodes.getCodeName(errorCode);
|
||||
mTextViewCompletion.setText(state + " " + errorText + "/" + errorCode);
|
||||
}
|
||||
|
||||
private void loadConfigsToSpinner(List<UpdateConfig> configs) {
|
||||
@@ -259,19 +283,42 @@ public class MainActivity extends Activity {
|
||||
/**
|
||||
* Applies the given update
|
||||
*/
|
||||
private void applyUpdate(UpdateConfig config) {
|
||||
if (config.getInstallType() == UpdateConfig.TYPE_NON_STREAMING) {
|
||||
AbNonStreamingUpdate update = new AbNonStreamingUpdate(mUpdateEngine, config);
|
||||
private void applyUpdate(final UpdateConfig config) {
|
||||
if (config.getInstallType() == UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING) {
|
||||
PayloadSpec payload;
|
||||
try {
|
||||
update.execute();
|
||||
} catch (Exception e) {
|
||||
Log.e("MainActivity", "Error applying the update", e);
|
||||
Toast.makeText(this, "Error applying the update", Toast.LENGTH_SHORT)
|
||||
payload = PayloadSpecs.forNonStreaming(config.getUpdatePackageFile());
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Error creating payload spec", e);
|
||||
Toast.makeText(this, "Error creating payload spec", Toast.LENGTH_LONG)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
updateEngineApplyPayload(payload);
|
||||
} else {
|
||||
Toast.makeText(this, "Streaming is not implemented", Toast.LENGTH_SHORT)
|
||||
.show();
|
||||
Log.d(TAG, "Starting PrepareStreamingService");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies given payload.
|
||||
*
|
||||
* UpdateEngine works asynchronously. This method doesn't wait until
|
||||
* end of the update.
|
||||
*/
|
||||
private void updateEngineApplyPayload(PayloadSpec payloadSpec) {
|
||||
try {
|
||||
mUpdateEngine.applyPayload(
|
||||
payloadSpec.getUrl(),
|
||||
payloadSpec.getOffset(),
|
||||
payloadSpec.getSize(),
|
||||
payloadSpec.getProperties().toArray(new String[0]));
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "UpdateEngine failed to apply the update", e);
|
||||
Toast.makeText(
|
||||
this,
|
||||
"UpdateEngine failed to apply the update",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,10 +327,11 @@ public class MainActivity extends Activity {
|
||||
* leave it as is.
|
||||
*/
|
||||
private void stopRunningUpdate() {
|
||||
Toast.makeText(this,
|
||||
"stopRunningUpdate is not implemented",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
|
||||
try {
|
||||
mUpdateEngine.cancel();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,13 +339,15 @@ public class MainActivity extends Activity {
|
||||
* update has been applied.
|
||||
*/
|
||||
private void resetUpdate() {
|
||||
Toast.makeText(this,
|
||||
"resetUpdate is not implemented",
|
||||
Toast.LENGTH_SHORT).show();
|
||||
try {
|
||||
mUpdateEngine.resetStatus();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "UpdateEngine failed to reset the update", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to delegate UpdateEngine callbacks to MainActivity
|
||||
* Helper class to delegate {@code update_engine} callbacks to MainActivity
|
||||
*/
|
||||
class UpdateEngineCallbackImpl extends UpdateEngineCallback {
|
||||
@Override
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.android.systemupdatersample.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,93 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
/**
|
||||
* Downloads chunk of a file from given url using {@code offset} and {@code size},
|
||||
* and saves to a given location.
|
||||
*
|
||||
* In real-life application this helper class should download from HTTP Server,
|
||||
* but in this sample app it will only download from a local file.
|
||||
*/
|
||||
public final class FileDownloader {
|
||||
|
||||
private String mUrl;
|
||||
private long mOffset;
|
||||
private long mSize;
|
||||
private File mOut;
|
||||
|
||||
public FileDownloader(String url, long offset, long size, File out) {
|
||||
this.mUrl = url;
|
||||
this.mOffset = offset;
|
||||
this.mSize = size;
|
||||
this.mOut = out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the file with given offset and size.
|
||||
*/
|
||||
public void download() throws IOException {
|
||||
Log.d("FileDownloader", "downloading " + mOut.getName()
|
||||
+ " from " + mUrl
|
||||
+ " to " + mOut.getAbsolutePath());
|
||||
|
||||
URL url = new URL(mUrl);
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.connect();
|
||||
|
||||
// download the file
|
||||
try (InputStream input = connection.getInputStream()) {
|
||||
try (OutputStream output = new FileOutputStream(mOut)) {
|
||||
long skipped = input.skip(mOffset);
|
||||
if (skipped != mOffset) {
|
||||
throw new IOException("Can't download file "
|
||||
+ mUrl
|
||||
+ " with given offset "
|
||||
+ mOffset);
|
||||
}
|
||||
byte[] data = new byte[4096];
|
||||
long total = 0;
|
||||
while (total < mSize) {
|
||||
int needToRead = (int) Math.min(4096, mSize - total);
|
||||
int count = input.read(data, 0, needToRead);
|
||||
if (count <= 0) {
|
||||
break;
|
||||
}
|
||||
output.write(data, 0, count);
|
||||
total += count;
|
||||
}
|
||||
if (total != mSize) {
|
||||
throw new IOException("Can't download file "
|
||||
+ mUrl
|
||||
+ " with given size "
|
||||
+ mSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+22
-5
@@ -16,13 +16,30 @@
|
||||
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
/** Utility class for property files in a package. */
|
||||
public final class PackagePropertyFiles {
|
||||
/** Utility class for an OTA package. */
|
||||
public final class PackageFiles {
|
||||
|
||||
/**
|
||||
* Directory used to perform updates.
|
||||
*/
|
||||
public static final String OTA_PACKAGE_DIR = "/data/ota_package";
|
||||
|
||||
/**
|
||||
* update payload, it will be passed to {@code UpdateEngine#applyPayload}.
|
||||
*/
|
||||
public static final String PAYLOAD_BINARY_FILE_NAME = "payload.bin";
|
||||
|
||||
public static final String PAYLOAD_HEADER_FILE_NAME = "payload_header.bin";
|
||||
|
||||
/**
|
||||
* Currently, when calling {@code UpdateEngine#applyPayload} to perform actions
|
||||
* that don't require network access (e.g. change slot), update_engine still
|
||||
* talks to the server to download/verify file.
|
||||
* {@code update_engine} might throw error when rebooting if {@code UpdateEngine#applyPayload}
|
||||
* is not supplied right headers and tokens.
|
||||
* This behavior might change in future android versions.
|
||||
*
|
||||
* To avoid extra network request in {@code update_engine}, this file has to be
|
||||
* downloaded and put in {@code OTA_PACKAGE_DIR}.
|
||||
*/
|
||||
public static final String PAYLOAD_METADATA_FILE_NAME = "payload_metadata.bin";
|
||||
|
||||
public static final String PAYLOAD_PROPERTIES_FILE_NAME = "payload_properties.txt";
|
||||
@@ -38,5 +55,5 @@ public final class PackagePropertyFiles {
|
||||
*/
|
||||
public static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
|
||||
|
||||
private PackagePropertyFiles() {}
|
||||
private PackageFiles() {}
|
||||
}
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
|
||||
import com.example.android.systemupdatersample.PayloadSpec;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
@@ -26,6 +23,7 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Enumeration;
|
||||
@@ -34,7 +32,6 @@ 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 {
|
||||
|
||||
/**
|
||||
@@ -68,14 +65,14 @@ public final class PayloadSpecs {
|
||||
}
|
||||
|
||||
long length = entry.getCompressedSize();
|
||||
if (PackagePropertyFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
|
||||
if (PackageFiles.PAYLOAD_BINARY_FILE_NAME.equals(name)) {
|
||||
if (entry.getMethod() != ZipEntry.STORED) {
|
||||
throw new IOException("Invalid compression method.");
|
||||
}
|
||||
payloadFound = true;
|
||||
payloadOffset = offset;
|
||||
payloadSize = length;
|
||||
} else if (PackagePropertyFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
|
||||
} else if (PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME.equals(name)) {
|
||||
InputStream inputStream = zip.getInputStream(entry);
|
||||
if (inputStream != null) {
|
||||
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
|
||||
@@ -100,6 +97,21 @@ public final class PayloadSpecs {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link PayloadSpec} for streaming update.
|
||||
*/
|
||||
public static PayloadSpec forStreaming(String updateUrl,
|
||||
long offset,
|
||||
long size,
|
||||
File propertiesFile) throws IOException {
|
||||
return PayloadSpec.newBuilder()
|
||||
.url(updateUrl)
|
||||
.offset(offset)
|
||||
.size(size)
|
||||
.properties(Files.readAllLines(propertiesFile.toPath()))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an {@link PayloadSpec} to a string.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.android.systemupdatersample.UpdateConfig;
|
||||
|
||||
@@ -70,6 +71,7 @@ public final class UpdateConfigs {
|
||||
StandardCharsets.UTF_8);
|
||||
configs.add(UpdateConfig.fromJson(json));
|
||||
} catch (Exception e) {
|
||||
Log.e("UpdateConfigs", "Can't read/parse config file " + f.getName(), e);
|
||||
throw new RuntimeException(
|
||||
"Can't read/parse config file " + f.getName(), e);
|
||||
}
|
||||
|
||||
+1
@@ -50,6 +50,7 @@ public final class UpdateEngineErrorCodes {
|
||||
CODE_TO_NAME_MAP.put(10, "PAYLOAD_HASH_MISMATCH_ERROR");
|
||||
CODE_TO_NAME_MAP.put(11, "PAYLOAD_SIZE_MISMATCH_ERROR");
|
||||
CODE_TO_NAME_MAP.put(12, "DOWNLOAD_PAYLOAD_VERIFICATION_ERROR");
|
||||
CODE_TO_NAME_MAP.put(15, "NEW_ROOTFS_VERIFICATION_ERROR");
|
||||
CODE_TO_NAME_MAP.put(20, "DOWNLOAD_STATE_INITIALIZATION_ERROR");
|
||||
CODE_TO_NAME_MAP.put(48, "USER_CANCELLED");
|
||||
CODE_TO_NAME_MAP.put(52, "UPDATED_BUT_NOT_ACTIVE");
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import android.util.SparseArray;
|
||||
|
||||
/**
|
||||
* Helper class to work with update_engine's error codes.
|
||||
* Many error codes are defined in {@link UpdateEngine.UpdateStatusConstants},
|
||||
* Many error codes are defined in {@code UpdateEngine.UpdateStatusConstants},
|
||||
* but you can find more in system/update_engine/common/error_code.h.
|
||||
*/
|
||||
public final class UpdateEngineStatuses {
|
||||
|
||||
@@ -22,11 +22,15 @@ LOCAL_SDK_VERSION := system_current
|
||||
LOCAL_MODULE_TAGS := tests
|
||||
LOCAL_JAVA_LIBRARIES := \
|
||||
android.test.base.stubs \
|
||||
android.test.runner.stubs
|
||||
android.test.runner.stubs \
|
||||
guava \
|
||||
mockito-target-minus-junit4
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := android-support-test
|
||||
LOCAL_INSTRUMENTATION_FOR := SystemUpdaterSample
|
||||
LOCAL_PROGUARD_ENABLED := disabled
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-subdir-java-files)
|
||||
LOCAL_RESOURCE_DIR := $(LOCAL_PATH)/res
|
||||
|
||||
LOCAL_SRC_FILES := $(call all-java-files-under, src)
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.example.android.systemupdatersample.tests">
|
||||
|
||||
<uses-sdk android:minSdkVersion="27" android:targetSdkVersion="27" />
|
||||
|
||||
<!-- We add an application tag here just so that we can indicate that
|
||||
this package needs to link against the android.test library,
|
||||
which is needed when building test cases. -->
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "streaming-001",
|
||||
"url": "http://foo.bar/update.zip",
|
||||
"type": "STREAMING",
|
||||
"streaming_metadata": {
|
||||
"ab_install_type": "STREAMING",
|
||||
"ab_streaming_metadata": {
|
||||
"property_files": [
|
||||
{
|
||||
"filename": "payload.bin",
|
||||
"offset": 531,
|
||||
"size": 5012323
|
||||
"offset": 195,
|
||||
"size": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+44
-10
@@ -19,14 +19,23 @@ package com.example.android.systemupdatersample;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.example.android.systemupdatersample.tests.R;
|
||||
import com.google.common.io.CharStreams;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Tests for {@link UpdateConfig}
|
||||
*/
|
||||
@@ -36,27 +45,48 @@ 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\"}";
|
||||
+ " \"ab_install_type\": \"NON_STREAMING\"}";
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Context mContext;
|
||||
private Context mTargetContext;
|
||||
private String mJsonStreaming001;
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
mContext = InstrumentationRegistry.getContext();
|
||||
mTargetContext = InstrumentationRegistry.getTargetContext();
|
||||
mJsonStreaming001 = readResource(R.raw.update_config_stream_001);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromJson_parsesJsonConfigWithoutMetadata() throws Exception {
|
||||
public void fromJson_parsesNonStreaming() throws Exception {
|
||||
UpdateConfig config = UpdateConfig.fromJson(JSON_NON_STREAMING);
|
||||
assertEquals("name is parsed", "vip update", config.getName());
|
||||
assertEquals("stores raw json", JSON_NON_STREAMING, config.getRawJson());
|
||||
assertSame("type is parsed", UpdateConfig.TYPE_NON_STREAMING, config.getInstallType());
|
||||
assertSame("type is parsed",
|
||||
UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING,
|
||||
config.getInstallType());
|
||||
assertEquals("url is parsed", "file:///builds/a.zip", config.getUrl());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromJson_parsesStreaming() throws Exception {
|
||||
UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
|
||||
assertEquals("streaming-001", config.getName());
|
||||
assertEquals("http://foo.bar/update.zip", config.getUrl());
|
||||
assertSame(UpdateConfig.AB_INSTALL_TYPE_STREAMING, config.getInstallType());
|
||||
assertEquals("payload.bin",
|
||||
config.getStreamingMetadata().getPropertyFiles()[0].getFilename());
|
||||
assertEquals(195, config.getStreamingMetadata().getPropertyFiles()[0].getOffset());
|
||||
assertEquals(8, config.getStreamingMetadata().getPropertyFiles()[0].getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getUpdatePackageFile_throwsErrorIfStreaming() throws Exception {
|
||||
UpdateConfig config = UpdateConfig.fromJson(JSON_STREAMING);
|
||||
UpdateConfig config = UpdateConfig.fromJson(mJsonStreaming001);
|
||||
thrown.expect(RuntimeException.class);
|
||||
config.getUpdatePackageFile();
|
||||
}
|
||||
@@ -64,7 +94,7 @@ public class UpdateConfigTest {
|
||||
@Test
|
||||
public void getUpdatePackageFile_throwsErrorIfNotAFile() throws Exception {
|
||||
String json = "{\"name\": \"upd\", \"url\": \"http://foo.bar\","
|
||||
+ " \"type\": \"NON_STREAMING\"}";
|
||||
+ " \"ab_install_type\": \"NON_STREAMING\"}";
|
||||
UpdateConfig config = UpdateConfig.fromJson(json);
|
||||
thrown.expect(RuntimeException.class);
|
||||
config.getUpdatePackageFile();
|
||||
@@ -73,7 +103,11 @@ public class UpdateConfigTest {
|
||||
@Test
|
||||
public void getUpdatePackageFile_works() throws Exception {
|
||||
UpdateConfig c = UpdateConfig.fromJson(JSON_NON_STREAMING);
|
||||
assertEquals("correct path", "/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
|
||||
assertEquals("/builds/a.zip", c.getUpdatePackageFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
private String readResource(int id) throws IOException {
|
||||
return CharStreams.toString(new InputStreamReader(
|
||||
mContext.getResources().openRawResource(id)));
|
||||
}
|
||||
}
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.example.android.systemupdatersample.util;
|
||||
|
||||
import static junit.framework.Assert.assertEquals;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.example.android.systemupdatersample.tests.R;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Tests for {@link FileDownloader}
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@SmallTest
|
||||
public class FileDownloaderTest {
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private Context mTestContext;
|
||||
private Context mTargetContext;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mTestContext = InstrumentationRegistry.getContext();
|
||||
mTargetContext = InstrumentationRegistry.getTargetContext();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void download_downloadsChunkOfZip() throws Exception {
|
||||
// Prepare the target file
|
||||
File packageFile = Paths
|
||||
.get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
|
||||
.toFile();
|
||||
Files.deleteIfExists(packageFile.toPath());
|
||||
Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
|
||||
packageFile.toPath());
|
||||
String url = "file://" + packageFile.getAbsolutePath();
|
||||
// prepare where to download
|
||||
File outFile = Paths
|
||||
.get(mTargetContext.getCacheDir().getAbsolutePath(), "care_map.txt")
|
||||
.toFile();
|
||||
Files.deleteIfExists(outFile.toPath());
|
||||
// download a chunk of ota.zip
|
||||
FileDownloader downloader = new FileDownloader(url, 160, 8, outFile);
|
||||
downloader.download();
|
||||
String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath()));
|
||||
// archive contains text files with uppercase filenames
|
||||
assertEquals("CARE_MAP", downloadedContent);
|
||||
}
|
||||
|
||||
}
|
||||
+29
-5
@@ -16,8 +16,9 @@
|
||||
|
||||
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 com.example.android.systemupdatersample.util.PackageFiles.PAYLOAD_BINARY_FILE_NAME;
|
||||
import static com.example.android.systemupdatersample.util.PackageFiles
|
||||
.PAYLOAD_PROPERTIES_FILE_NAME;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
@@ -28,6 +29,8 @@ import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import com.example.android.systemupdatersample.PayloadSpec;
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.io.Files;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -56,16 +59,16 @@ public class PayloadSpecsTest {
|
||||
|
||||
private File mTestDir;
|
||||
|
||||
private Context mContext;
|
||||
private Context mTargetContext;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mContext = InstrumentationRegistry.getTargetContext();
|
||||
mTargetContext = InstrumentationRegistry.getTargetContext();
|
||||
|
||||
mTestDir = mContext.getFilesDir();
|
||||
mTestDir = mTargetContext.getFilesDir();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,6 +90,21 @@ public class PayloadSpecsTest {
|
||||
PayloadSpecs.forNonStreaming(new File("/fake/news.zip"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forStreaming_works() throws Exception {
|
||||
String url = "http://a.com/b.zip";
|
||||
long offset = 45;
|
||||
long size = 200;
|
||||
File propertiesFile = createMockPropertiesFile();
|
||||
|
||||
PayloadSpec spec = PayloadSpecs.forStreaming(url, offset, size, propertiesFile);
|
||||
assertEquals("same url", url, spec.getUrl());
|
||||
assertEquals("same offset", offset, spec.getOffset());
|
||||
assertEquals("same size", size, spec.getSize());
|
||||
assertArrayEquals("correct properties",
|
||||
new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates package zip file that contains payload.bin and payload_properties.txt
|
||||
*/
|
||||
@@ -114,4 +132,10 @@ public class PayloadSpecsTest {
|
||||
return testFile;
|
||||
}
|
||||
|
||||
private File createMockPropertiesFile() throws IOException {
|
||||
File propertiesFile = new File(mTestDir, PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME);
|
||||
Files.asCharSink(propertiesFile, Charsets.UTF_8).write(PROPERTIES_CONTENTS);
|
||||
return propertiesFile;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-12
@@ -18,14 +18,11 @@ 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;
|
||||
@@ -41,21 +38,14 @@ import java.util.List;
|
||||
@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)
|
||||
new UpdateConfig("blah", "http://", UpdateConfig.AB_INSTALL_TYPE_NON_STREAMING),
|
||||
new UpdateConfig("blah 2", "http://", UpdateConfig.AB_INSTALL_TYPE_STREAMING)
|
||||
);
|
||||
String[] names = UpdateConfigs.configsToNames(configs);
|
||||
assertArrayEquals(new String[] {"blah", "blah 2"}, names);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"""
|
||||
Given a OTA package file, produces update config JSON file.
|
||||
|
||||
Example: tools/gen_update.config.py \\
|
||||
Example: tools/gen_update_config.py \\
|
||||
--ab_install_type=STREAMING \\
|
||||
ota-build-001.zip \\
|
||||
my-config-001.json \\
|
||||
|
||||
@@ -39,9 +39,9 @@ void VrRecoveryUI::DrawTextIcon(int x, int y, GRSurface* surface) const {
|
||||
gr_texticon(x - kStereoOffset + ScreenWidth(), y, surface);
|
||||
}
|
||||
|
||||
int VrRecoveryUI::DrawTextLine(int x, int y, const char* line, bool bold) const {
|
||||
gr_text(gr_sys_font(), x + kStereoOffset, y, line, bold);
|
||||
gr_text(gr_sys_font(), x - kStereoOffset + ScreenWidth(), y, line, bold);
|
||||
int VrRecoveryUI::DrawTextLine(int x, int y, const std::string& line, bool bold) const {
|
||||
gr_text(gr_sys_font(), x + kStereoOffset, y, line.c_str(), bold);
|
||||
gr_text(gr_sys_font(), x - kStereoOffset + ScreenWidth(), y, line.c_str(), bold);
|
||||
return char_height_ + 4;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#ifndef RECOVERY_VR_UI_H
|
||||
#define RECOVERY_VR_UI_H
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "screen_ui.h"
|
||||
|
||||
class VrRecoveryUI : public ScreenRecoveryUI {
|
||||
@@ -36,7 +38,7 @@ class VrRecoveryUI : public ScreenRecoveryUI {
|
||||
void DrawHighlightBar(int x, int y, int width, int height) const override;
|
||||
void DrawFill(int x, int y, int w, int h) const override;
|
||||
void DrawTextIcon(int x, int y, GRSurface* surface) const override;
|
||||
int DrawTextLine(int x, int y, const char* line, bool bold) const override;
|
||||
int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
|
||||
};
|
||||
|
||||
#endif // RECOVERY_VR_UI_H
|
||||
|
||||
+12
-13
@@ -20,6 +20,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <android-base/properties.h>
|
||||
#include <android-base/strings.h>
|
||||
@@ -61,13 +62,6 @@ void WearRecoveryUI::draw_background_locked() {
|
||||
}
|
||||
}
|
||||
|
||||
static const char* SWIPE_HELP[] = {
|
||||
"Swipe up/down to move.",
|
||||
"Swipe left/right to select.",
|
||||
"",
|
||||
nullptr,
|
||||
};
|
||||
|
||||
void WearRecoveryUI::draw_screen_locked() {
|
||||
draw_background_locked();
|
||||
if (!show_text) {
|
||||
@@ -76,6 +70,13 @@ void WearRecoveryUI::draw_screen_locked() {
|
||||
SetColor(TEXT_FILL);
|
||||
gr_fill(0, 0, gr_fb_width(), gr_fb_height());
|
||||
|
||||
// clang-format off
|
||||
static std::vector<std::string> SWIPE_HELP = {
|
||||
"Swipe up/down to move.",
|
||||
"Swipe left/right to select.",
|
||||
"",
|
||||
};
|
||||
// clang-format on
|
||||
draw_menu_and_text_buffer_locked(SWIPE_HELP);
|
||||
}
|
||||
}
|
||||
@@ -88,15 +89,13 @@ void WearRecoveryUI::update_progress_locked() {
|
||||
|
||||
void WearRecoveryUI::SetStage(int /* current */, int /* max */) {}
|
||||
|
||||
void WearRecoveryUI::StartMenu(const char* const* headers, const char* const* items,
|
||||
int initial_selection) {
|
||||
void WearRecoveryUI::StartMenu(const std::vector<std::string>& headers,
|
||||
const std::vector<std::string>& items, size_t initial_selection) {
|
||||
pthread_mutex_lock(&updateMutex);
|
||||
if (text_rows_ > 0 && text_cols_ > 0) {
|
||||
menu_ = std::make_unique<Menu>(scrollable_menu_, text_rows_ - kMenuUnusableRows - 1,
|
||||
text_cols_ - 1);
|
||||
menu_->Start(headers, items, initial_selection);
|
||||
|
||||
text_cols_ - 1, headers, items, initial_selection);
|
||||
update_screen_locked();
|
||||
}
|
||||
pthread_mutex_unlock(&updateMutex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#ifndef RECOVERY_WEAR_UI_H
|
||||
#define RECOVERY_WEAR_UI_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "screen_ui.h"
|
||||
|
||||
class WearRecoveryUI : public ScreenRecoveryUI {
|
||||
@@ -25,9 +28,6 @@ class WearRecoveryUI : public ScreenRecoveryUI {
|
||||
|
||||
void SetStage(int current, int max) override;
|
||||
|
||||
void StartMenu(const char* const* headers, const char* const* items,
|
||||
int initial_selection) override;
|
||||
|
||||
protected:
|
||||
// progress bar vertical position, it's centered horizontally
|
||||
const int kProgressBarBaseline;
|
||||
@@ -36,6 +36,9 @@ class WearRecoveryUI : public ScreenRecoveryUI {
|
||||
// Recovery, build id and etc) and the bottom lines that may otherwise go out of the screen.
|
||||
const int kMenuUnusableRows;
|
||||
|
||||
void StartMenu(const std::vector<std::string>& headers, const std::vector<std::string>& items,
|
||||
size_t initial_selection) override;
|
||||
|
||||
int GetProgressBaseline() const override;
|
||||
|
||||
void update_progress_locked() override;
|
||||
|
||||
Reference in New Issue
Block a user