Snap for 4762932 from 96106592a2cebb78cc7d2772376cd1caf34ce9a1 to qt-release
Change-Id: I003a1ed8fe9bbb969efec6105556c5adc38f79a8
diff --git a/Android.mk b/Android.mk
index 5b71bd3..57b1803 100644
--- a/Android.mk
+++ b/Android.mk
@@ -126,6 +126,7 @@
device.cpp \
fuse_sdcard_provider.cpp \
recovery.cpp \
+ recovery_main.cpp \
roots.cpp \
rotate_logs.cpp \
diff --git a/common.h b/common.h
index 4228e71..33c5ba0 100644
--- a/common.h
+++ b/common.h
@@ -37,9 +37,13 @@
// 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();
diff --git a/device.cpp b/device.cpp
index 3b0942c..5cf9cc2 100644
--- a/device.cpp
+++ b/device.cpp
@@ -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 @@
"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 @@
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) {
diff --git a/device.h b/device.h
index 4ea3159..8788b2d 100644
--- a/device.h
+++ b/device.h
@@ -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 @@
//
// 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
diff --git a/private/recovery.h b/private/recovery.h
new file mode 100644
index 0000000..5b2ca4b
--- /dev/null
+++ b/private/recovery.h
@@ -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);
diff --git a/recovery.cpp b/recovery.cpp
index 5fc3b1a..d7ece4e 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -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 @@
* 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 @@
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 @@
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 @@
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 @@
}
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 @@
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 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 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 @@
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 @@
}
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 @@
}
}
-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 @@
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();
diff --git a/recovery_main.cpp b/recovery_main.cpp
new file mode 100644
index 0000000..9f579f7
--- /dev/null
+++ b/recovery_main.cpp
@@ -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);
+}
diff --git a/screen_ui.cpp b/screen_ui.cpp
index 317e552..7ae81e5 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -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 @@
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 @@
}
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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
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 @@
// 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 @@
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(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 @@
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 @@
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;
diff --git a/screen_ui.h b/screen_ui.h
index c1222a5..fb811ce 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -20,6 +20,7 @@
#include <pthread.h>
#include <stdio.h>
+#include <functional>
#include <memory>
#include <string>
#include <vector>
@@ -32,20 +33,26 @@
// 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 @@
// /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 @@
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 @@
// 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 @@
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 @@
// 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 @@
// 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;
diff --git a/stub_ui.h b/stub_ui.h
index 1f6b29a..2ccd491 100644
--- a/stub_ui.h
+++ b/stub_ui.h
@@ -17,6 +17,10 @@
#ifndef RECOVERY_STUB_UI_H
#define RECOVERY_STUB_UI_H
+#include <functional>
+#include <string>
+#include <vector>
+
#include "ui.h"
// Stub implementation of RecoveryUI for devices without screen.
@@ -51,15 +55,15 @@
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
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
index be6799f..e47d705 100644
--- a/tests/unit/screen_ui_test.cpp
+++ b/tests/unit/screen_ui_test.cpp
@@ -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, 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, 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, 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, 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, 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, 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.
diff --git a/ui.h b/ui.h
index 4c54d69..35cc36e 100644
--- a/ui.h
+++ b/ui.h
@@ -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 @@
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 @@
// --- 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);
diff --git a/updater_sample/Android.mk b/updater_sample/Android.mk
index 2786de4..056ad66 100644
--- a/updater_sample/Android.mk
+++ b/updater_sample/Android.mk
@@ -26,6 +26,10 @@
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.
diff --git a/updater_sample/README.md b/updater_sample/README.md
index ee1faaf..12f803f 100644
--- a/updater_sample/README.md
+++ b/updater_sample/README.md
@@ -30,13 +30,19 @@
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 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 @@
```
-## 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).
diff --git a/updater_sample/res/layout/activity_main.xml b/updater_sample/res/layout/activity_main.xml
index 3cd7721..7a12d34 100644
--- a/updater_sample/res/layout/activity_main.xml
+++ b/updater_sample/res/layout/activity_main.xml
@@ -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"
diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json
index 03335cc..b6f4cdc 100644
--- a/updater_sample/res/raw/sample.json
+++ b/updater_sample/res/raw/sample.json
@@ -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
diff --git a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
index 90c5637..ce88338 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/PayloadSpec.java
@@ -18,12 +18,15 @@
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}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
index cbee18f..23510e4 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -19,6 +19,7 @@
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.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 @@
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 @@
/**
* 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 @@
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 @@
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 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 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 long getSize() {
return mSize;
}
+
}
}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
index 72e1b24..d6a6ce3 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -31,13 +31,15 @@
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 @@
*/
public class MainActivity extends Activity {
+ private static final String TAG = "MainActivity";
+
private TextView mTextViewBuild;
private Spinner mSpinnerConfigs;
private TextView mTextViewConfigsDirHint;
@@ -55,17 +59,19 @@
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 @@
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 @@
.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 @@
+ " 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 @@
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 @@
* 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 @@
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 @@
*/
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 @@
/**
* 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 @@
* 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 @@
* 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
diff --git a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java b/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
deleted file mode 100644
index 1b91a1a..0000000
--- a/updater_sample/src/com/example/android/systemupdatersample/updates/AbNonStreamingUpdate.java
+++ /dev/null
@@ -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]));
- }
-
-}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
new file mode 100644
index 0000000..5c1d711
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -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);
+ }
+ }
+ }
+ }
+
+}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
similarity index 60%
rename from updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java
rename to updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
index 3988b59..b485234 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/PackagePropertyFiles.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PackageFiles.java
@@ -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 static final String COMPATIBILITY_ZIP_FILE_NAME = "compatibility.zip";
- private PackagePropertyFiles() {}
+ private PackageFiles() {}
}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
index 43c8d75..4db448a 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/PayloadSpecs.java
@@ -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.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.ZipFile;
/** The helper class that creates {@link PayloadSpec}. */
-@TargetApi(Build.VERSION_CODES.N)
public final class PayloadSpecs {
/**
@@ -68,14 +65,14 @@
}
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));
@@ -101,6 +98,21 @@
}
/**
+ * 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.
*/
public static String toString(PayloadSpec payloadSpec) {
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
index 089f8b2..71d4df8 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -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 @@
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);
}
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
index e63da62..6d319c5 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineErrorCodes.java
@@ -50,6 +50,7 @@
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");
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
index 6203b20..a96f19d 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateEngineStatuses.java
@@ -20,7 +20,7 @@
/**
* 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 {
diff --git a/updater_sample/tests/Android.mk b/updater_sample/tests/Android.mk
index 83082cd..a1a4664 100644
--- a/updater_sample/tests/Android.mk
+++ b/updater_sample/tests/Android.mk
@@ -22,11 +22,15 @@
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)
diff --git a/updater_sample/tests/AndroidManifest.xml b/updater_sample/tests/AndroidManifest.xml
index 2392bb3..76af5f1 100644
--- a/updater_sample/tests/AndroidManifest.xml
+++ b/updater_sample/tests/AndroidManifest.xml
@@ -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. -->
diff --git a/updater_sample/tests/res/raw/update_config_stream_001.json b/updater_sample/tests/res/raw/update_config_stream_001.json
index 965f737..15127cf 100644
--- a/updater_sample/tests/res/raw/update_config_stream_001.json
+++ b/updater_sample/tests/res/raw/update_config_stream_001.json
@@ -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
}
]
}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
index 8715371..0975e76 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -19,14 +19,23 @@
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 @@
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 @@
@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 @@
@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)));
+ }
}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
new file mode 100644
index 0000000..80506ee
--- /dev/null
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example.android.systemupdatersample.util;
+
+import static 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);
+ }
+
+}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
index 6f06ca3..2912e20 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/PayloadSpecsTest.java
@@ -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.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 @@
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 @@
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 @@
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;
+ }
+
}
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
index 4aa8c64..4ccae93 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/UpdateConfigsTest.java
@@ -18,14 +18,11 @@
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 @@
@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);
diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py
index cb9bd01..0578124 100755
--- a/updater_sample/tools/gen_update_config.py
+++ b/updater_sample/tools/gen_update_config.py
@@ -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 \\
diff --git a/vr_ui.cpp b/vr_ui.cpp
index a58c99e..b1ef646 100644
--- a/vr_ui.cpp
+++ b/vr_ui.cpp
@@ -39,9 +39,9 @@
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;
}
diff --git a/vr_ui.h b/vr_ui.h
index eeb4589..08384ce 100644
--- a/vr_ui.h
+++ b/vr_ui.h
@@ -17,6 +17,8 @@
#ifndef RECOVERY_VR_UI_H
#define RECOVERY_VR_UI_H
+#include <string>
+
#include "screen_ui.h"
class VrRecoveryUI : public ScreenRecoveryUI {
@@ -36,7 +38,7 @@
void DrawHighlightBar(int x, int y, int width, int height) const override;
void DrawFill(int x, int y, int w, int h) const override;
void DrawTextIcon(int x, int y, GRSurface* surface) const override;
- int DrawTextLine(int x, int y, const char* line, bool bold) const override;
+ int DrawTextLine(int x, int y, const std::string& line, bool bold) const override;
};
#endif // RECOVERY_VR_UI_H
diff --git a/wear_ui.cpp b/wear_ui.cpp
index 118e435..f157d3c 100644
--- a/wear_ui.cpp
+++ b/wear_ui.cpp
@@ -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 @@
}
}
-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 @@
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::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);
-}
\ No newline at end of file
+}
diff --git a/wear_ui.h b/wear_ui.h
index 8b24cb7..c9a9f0e 100644
--- a/wear_ui.h
+++ b/wear_ui.h
@@ -17,6 +17,9 @@
#ifndef RECOVERY_WEAR_UI_H
#define RECOVERY_WEAR_UI_H
+#include <string>
+#include <vector>
+
#include "screen_ui.h"
class WearRecoveryUI : public ScreenRecoveryUI {
@@ -25,9 +28,6 @@
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 @@
// 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;