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;