Merge "screen_ui: Fix an issue in RTL locale detection."
diff --git a/.clang-format b/.clang-format
index 0e0f4d1..4a3bd2f 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,3 +1,33 @@
+# bootable/recovery project uses repohook to apply `clang-format` to the changed lines, with the
+# local style file in `.clang-format`. This will be triggered automatically with `repo upload`.
+# Alternatively, one can stage and format a change with `git clang-format` directly.
+#
+#   $ git add <files>
+#   $ git clang-format --style file
+#
+# Or to format a committed change.
+#
+#   $ git clang-format --style file HEAD~1
+#
+# `--style file` will pick up the local style file in `.clang-format`. This can be configured as the
+# default behavior for bootable/recovery project.
+#
+#   $ git config --local clangFormat.style file
+#
+# Note that `repo upload` calls the `clang-format` binary in Android repo (i.e.
+# `$ANDROID_BUILD_TOP/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format`), which might
+# give slightly different results from the one installed in host machine (e.g.
+# `/usr/bin/clang-format`). Specifying the file with `--binary` will ensure consistent results.
+#
+#  $ git clang-format --binary \
+#      /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+# Or to do one-time setup to make it default.
+#
+#   $ git config --local clangFormat.binary \
+#       /path/to/aosp-master/prebuilts/clang/host/linux-x86/clang-stable/bin/clang-format
+#
+
 BasedOnStyle: Google
 AllowShortBlocksOnASingleLine: false
 AllowShortFunctionsOnASingleLine: Empty
diff --git a/Android.mk b/Android.mk
index e6bea07..a9631bf 100644
--- a/Android.mk
+++ b/Android.mk
@@ -23,6 +23,11 @@
 # librecovery_ui_default, which uses ScreenRecoveryUI.
 TARGET_RECOVERY_UI_LIB ?= librecovery_ui_default
 
+recovery_common_cflags := \
+    -Wall \
+    -Werror \
+    -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
+
 # librecovery (static library)
 # ===============================
 include $(CLEAR_VARS)
@@ -30,8 +35,7 @@
 LOCAL_SRC_FILES := \
     install.cpp
 
-LOCAL_CFLAGS := -Wall -Werror
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
+LOCAL_CFLAGS := $(recovery_common_cflags)
 
 ifeq ($(AB_OTA_UPDATER),true)
     LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
@@ -54,13 +58,12 @@
 # ===============================
 include $(CLEAR_VARS)
 LOCAL_SRC_FILES := \
+    device.cpp \
     screen_ui.cpp \
     ui.cpp \
     vr_ui.cpp \
     wear_ui.cpp
 
-LOCAL_CFLAGS := -Wall -Werror
-
 LOCAL_MODULE := librecovery_ui
 
 LOCAL_STATIC_LIBRARIES := \
@@ -68,6 +71,8 @@
     libotautil \
     libbase
 
+LOCAL_CFLAGS := $(recovery_common_cflags)
+
 ifneq ($(TARGET_RECOVERY_UI_MARGIN_HEIGHT),)
 LOCAL_CFLAGS += -DRECOVERY_UI_MARGIN_HEIGHT=$(TARGET_RECOVERY_UI_MARGIN_HEIGHT)
 else
@@ -124,7 +129,6 @@
 
 LOCAL_SRC_FILES := \
     adb_install.cpp \
-    device.cpp \
     fuse_sdcard_provider.cpp \
     logging.cpp \
     recovery.cpp \
@@ -147,8 +151,7 @@
 endif
 endif
 
-LOCAL_CFLAGS += -DRECOVERY_API_VERSION=$(RECOVERY_API_VERSION)
-LOCAL_CFLAGS += -Wall -Werror
+LOCAL_CFLAGS := $(recovery_common_cflags)
 
 LOCAL_C_INCLUDES += \
     system/vold \
@@ -185,10 +188,6 @@
 
 LOCAL_HAL_STATIC_LIBRARIES := libhealthd
 
-ifeq ($(AB_OTA_UPDATER),true)
-    LOCAL_CFLAGS += -DAB_OTA_UPDATER=1
-endif
-
 LOCAL_MODULE_PATH := $(TARGET_RECOVERY_ROOT_OUT)/sbin
 
 ifeq ($(BOARD_CACHEIMAGE_PARTITION_SIZE),)
diff --git a/device.cpp b/device.cpp
index 5cf9cc2..3c6334e 100644
--- a/device.cpp
+++ b/device.cpp
@@ -16,59 +16,57 @@
 
 #include "device.h"
 
+#include <algorithm>
+#include <string>
+#include <utility>
+#include <vector>
+
 #include <android-base/logging.h>
-#include <android-base/macros.h>
 
 #include "ui.h"
 
-// clang-format off
-static constexpr const char* kItems[]{
-  "Reboot system now",
-  "Reboot to bootloader",
-  "Apply update from ADB",
-  "Apply update from SD card",
-  "Wipe data/factory reset",
-#ifndef AB_OTA_UPDATER
-  "Wipe cache partition",
-#endif  // !AB_OTA_UPDATER
-  "Mount /system",
-  "View recovery logs",
-  "Run graphics test",
-  "Run locale test",
-  "Power off",
+static std::vector<std::pair<std::string, Device::BuiltinAction>> g_menu_actions{
+  { "Reboot system now", Device::REBOOT },
+  { "Reboot to bootloader", Device::REBOOT_BOOTLOADER },
+  { "Apply update from ADB", Device::APPLY_ADB_SIDELOAD },
+  { "Apply update from SD card", Device::APPLY_SDCARD },
+  { "Wipe data/factory reset", Device::WIPE_DATA },
+  { "Wipe cache partition", Device::WIPE_CACHE },
+  { "Mount /system", Device::MOUNT_SYSTEM },
+  { "View recovery logs", Device::VIEW_RECOVERY_LOGS },
+  { "Run graphics test", Device::RUN_GRAPHICS_TEST },
+  { "Run locale test", Device::RUN_LOCALE_TEST },
+  { "Power off", Device::SHUTDOWN },
 };
-// clang-format on
 
-// clang-format off
-static constexpr Device::BuiltinAction kMenuActions[] {
-  Device::REBOOT,
-  Device::REBOOT_BOOTLOADER,
-  Device::APPLY_ADB_SIDELOAD,
-  Device::APPLY_SDCARD,
-  Device::WIPE_DATA,
-#ifndef AB_OTA_UPDATER
-  Device::WIPE_CACHE,
-#endif  // !AB_OTA_UPDATER
-  Device::MOUNT_SYSTEM,
-  Device::VIEW_RECOVERY_LOGS,
-  Device::RUN_GRAPHICS_TEST,
-  Device::RUN_LOCALE_TEST,
-  Device::SHUTDOWN,
-};
-// clang-format on
+static std::vector<std::string> g_menu_items;
 
-static_assert(arraysize(kItems) == arraysize(kMenuActions),
-              "kItems and kMenuActions should have the same length.");
+static void PopulateMenuItems() {
+  g_menu_items.clear();
+  std::transform(g_menu_actions.cbegin(), g_menu_actions.cend(), std::back_inserter(g_menu_items),
+                 [](const auto& entry) { return entry.first; });
+}
 
-static const std::vector<std::string> kMenuItems(kItems, kItems + arraysize(kItems));
+Device::Device(RecoveryUI* ui) : ui_(ui) {
+  PopulateMenuItems();
+}
+
+void Device::RemoveMenuItemForAction(Device::BuiltinAction action) {
+  g_menu_actions.erase(
+      std::remove_if(g_menu_actions.begin(), g_menu_actions.end(),
+                     [action](const auto& entry) { return entry.second == action; }));
+  CHECK(!g_menu_actions.empty());
+
+  // Re-populate the menu items.
+  PopulateMenuItems();
+}
 
 const std::vector<std::string>& Device::GetMenuItems() {
-  return kMenuItems;
+  return g_menu_items;
 }
 
 Device::BuiltinAction Device::InvokeMenuItem(size_t menu_position) {
-  // CHECK_LT(menu_position, );
-  return kMenuActions[menu_position];
+  return g_menu_actions[menu_position].second;
 }
 
 int Device::HandleMenuKey(int key, bool visible) {
diff --git a/device.h b/device.h
index 9510fbe..9c43371 100644
--- a/device.h
+++ b/device.h
@@ -19,6 +19,7 @@
 
 #include <stddef.h>
 
+#include <memory>
 #include <string>
 #include <vector>
 
@@ -48,20 +49,18 @@
     RUN_LOCALE_TEST = 12,
   };
 
-  explicit Device(RecoveryUI* ui) : ui_(ui) {}
+  explicit Device(RecoveryUI* ui);
   virtual ~Device() {}
 
-  // Called to obtain the UI object that should be used to display the recovery user interface for
-  // this device. You should not have called Init() on the UI object already, the caller will do
-  // that after this method returns.
+  // Returns a raw pointer to the RecoveryUI object.
   virtual RecoveryUI* GetUI() {
-    return ui_;
+    return ui_.get();
   }
 
-  // Sets the UI object to the given UI. Used to override the default UI in case initialization
-  // failed, or we want a stub for some reason.
-  virtual void SetUI(RecoveryUI* ui) {
-    ui_ = ui;
+  // Resets the UI object to the given UI. Used to override the default UI in case initialization
+  // failed, or we want a different UI for some reason. The device object will take the ownership.
+  virtual void ResetUI(RecoveryUI* ui) {
+    ui_.reset(ui);
   }
 
   // Called when recovery starts up (after the UI has been obtained and initialized and after the
@@ -70,16 +69,15 @@
 
   // Called from the main thread when recovery is at the main menu and waiting for input, and a key
   // is pressed. (Note that "at" the main menu does not necessarily mean the menu is visible;
-  // recovery will be at the main menu with it invisible after an unsuccessful operation [ie OTA
-  // package failure], or if recovery is started with no command.)
+  // recovery will be at the main menu with it invisible after an unsuccessful operation, such as
+  // failed to install an OTA package, or if recovery is started with no command.)
   //
   // 'key' is the code of the key just pressed. (You can call IsKeyPressed() on the RecoveryUI
-  // object you returned from GetUI if you want to find out if other keys are held down.)
+  // object you returned from GetUI() if you want to find out if other keys are held down.)
   //
   // 'visible' is true if the menu is visible.
   //
   // Returns one of the defined constants below in order to:
-  //
   //   - move the menu highlight (kHighlight{Up,Down}: negative value)
   //   - invoke the highlighted item (kInvokeItem: negative value)
   //   - do nothing (kNoAction: negative value)
@@ -87,7 +85,7 @@
   virtual int HandleMenuKey(int key, bool visible);
 
   // Returns the list of menu items (a vector of strings). The menu_position passed to
-  // InvokeMenuItem will correspond to the indexes into this array.
+  // InvokeMenuItem() will correspond to the indexes into this array.
   virtual const std::vector<std::string>& GetMenuItems();
 
   // Performs a recovery action selected from the menu. 'menu_position' will be the index of the
@@ -98,6 +96,10 @@
   // here and return NO_ACTION.
   virtual BuiltinAction InvokeMenuItem(size_t menu_position);
 
+  // Removes the menu item for the given action. This allows tailoring the menu based on the
+  // runtime info, such as the availability of /cache or /sdcard.
+  virtual void RemoveMenuItemForAction(Device::BuiltinAction action);
+
   // 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
   // selects the option from the menu, to perform whatever device-specific wiping actions as needed.
@@ -112,7 +114,8 @@
   }
 
  private:
-  RecoveryUI* ui_;
+  // The RecoveryUI object that should be used to display the user interface for this device.
+  std::unique_ptr<RecoveryUI> ui_;
 };
 
 // The device-specific library must define this function (or the default one will be used, if there
diff --git a/recovery.cpp b/recovery.cpp
index 6aceb2d..e427998 100644
--- a/recovery.cpp
+++ b/recovery.cpp
@@ -64,7 +64,6 @@
 #include "fuse_sideload.h"
 #include "install.h"
 #include "logging.h"
-#include "minui/minui.h"
 #include "otautil/dirutil.h"
 #include "otautil/error_code.h"
 #include "otautil/paths.h"
@@ -231,8 +230,8 @@
 // Clear the recovery command and prepare to boot a (hopefully working) system,
 // copy our log file to cache as well (for the system to read). This function is
 // idempotent: call it as many times as you like.
-static void finish_recovery(Device* device) {
-  std::string locale = device->GetUI()->GetLocale();
+static void finish_recovery() {
+  std::string locale = ui->GetLocale();
   // Save the locale to cache, so if recovery is next started up without a '--locale' argument
   // (e.g., directly from the bootloader) it will use the last-known locale.
   if (!locale.empty() && has_cache) {
@@ -809,7 +808,7 @@
 // which is to reboot or shutdown depending on if the --shutdown_after flag was passed to recovery.
 static Device::BuiltinAction prompt_and_wait(Device* device, int status) {
   for (;;) {
-    finish_recovery(device);
+    finish_recovery();
     switch (status) {
       case INSTALL_SUCCESS:
       case INSTALL_NONE:
@@ -1174,16 +1173,18 @@
   Device* device = make_device();
   if (android::base::GetBoolProperty("ro.boot.quiescent", false)) {
     printf("Quiescent recovery mode.\n");
-    ui = new StubRecoveryUI();
+    device->ResetUI(new StubRecoveryUI());
   } else {
-    ui = device->GetUI();
-
-    if (!ui->Init(locale)) {
-      printf("Failed to initialize UI, use stub UI instead.\n");
-      ui = new StubRecoveryUI();
+    if (!device->GetUI()->Init(locale)) {
+      printf("Failed to initialize UI; using stub UI instead.\n");
+      device->ResetUI(new StubRecoveryUI());
     }
   }
-  device->SetUI(ui);
+  ui = device->GetUI();
+
+  if (!has_cache) {
+    device->RemoveMenuItemForAction(Device::WIPE_CACHE);
+  }
 
   // Set background string to "installing security update" for security update,
   // otherwise set it to "installing system update".
@@ -1350,7 +1351,7 @@
   }
 
   // Save logs and clean up before rebooting or shutting down.
-  finish_recovery(device);
+  finish_recovery();
 
   switch (after) {
     case Device::SHUTDOWN:
diff --git a/screen_ui.cpp b/screen_ui.cpp
index 2a8d7f9..fd7a1be 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -469,20 +469,22 @@
 
 int ScreenRecoveryUI::DrawWrappedTextLines(int x, int y,
                                            const std::vector<std::string>& lines) const {
+  // Keep symmetrical margins based on the given offset (i.e. x).
+  size_t text_cols = (ScreenWidth() - x * 2) / char_width_;
   int offset = 0;
   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);
-      if (sub.size() <= text_cols_) {
+      std::string sub = line.substr(next_start, text_cols + 1);
+      if (sub.size() <= text_cols) {
         next_start += sub.size();
       } else {
-        // Line too long and must be wrapped to text_cols_ columns.
+        // 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.
-          sub.resize(text_cols_);
-          next_start += text_cols_;
+          sub.resize(text_cols);
+          next_start += text_cols;
         } else {
           sub.resize(last_space);
           next_start += last_space + 1;
@@ -750,7 +752,7 @@
   return true;
 }
 
-std::string ScreenRecoveryUI::GetLocale() {
+std::string ScreenRecoveryUI::GetLocale() const {
   return locale_;
 }
 
diff --git a/screen_ui.h b/screen_ui.h
index d4923f5..2d6b621 100644
--- a/screen_ui.h
+++ b/screen_ui.h
@@ -114,7 +114,7 @@
   explicit ScreenRecoveryUI(bool scrollable_menu);
 
   bool Init(const std::string& locale) override;
-  std::string GetLocale() override;
+  std::string GetLocale() const override;
 
   // overall recovery state ("background image")
   void SetBackground(Icon icon) override;
@@ -224,8 +224,9 @@
   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 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.
+  // Similar to DrawTextLines() to draw multiple text lines, but additionally wraps long lines. It
+  // keeps symmetrical margins of 'x' at each end of a line. Returns the offset it should be moving
+  // along Y-axis.
   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 fddf4e7..67c338e 100644
--- a/stub_ui.h
+++ b/stub_ui.h
@@ -28,7 +28,7 @@
  public:
   StubRecoveryUI() = default;
 
-  std::string GetLocale() override {
+  std::string GetLocale() const override {
     return "";
   }
   void SetBackground(Icon /* icon */) override {}
diff --git a/ui.h b/ui.h
index f867790..3928426 100644
--- a/ui.h
+++ b/ui.h
@@ -57,7 +57,7 @@
   // the given locale. Returns true on success.
   virtual bool Init(const std::string& locale);
 
-  virtual std::string GetLocale() = 0;
+  virtual std::string GetLocale() const = 0;
 
   // Shows a stage indicator. Called immediately after Init().
   virtual void SetStage(int current, int max) = 0;
diff --git a/updater_sample/AndroidManifest.xml b/updater_sample/AndroidManifest.xml
index 5bbb21c..4b44484 100644
--- a/updater_sample/AndroidManifest.xml
+++ b/updater_sample/AndroidManifest.xml
@@ -31,6 +31,7 @@
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
+        <service android:name=".services.PrepareStreamingService"/>
     </application>
 
 </manifest>
diff --git a/updater_sample/README.md b/updater_sample/README.md
index 12f803f..95e57db 100644
--- a/updater_sample/README.md
+++ b/updater_sample/README.md
@@ -61,6 +61,17 @@
 6. Push OTA packages to the device.
 
 
+## Sending HTTP headers from UpdateEngine
+
+Sometimes OTA package server might require some HTTP headers to be present,
+e.g. `Authorization` header to contain valid auth token. While performing
+streaming update, `UpdateEngine` allows passing on certain HTTP headers;
+as of writing this sample app, these headers are `Authorization` and `User-Agent`.
+
+`android.os.UpdateEngine#applyPayload` contains information on
+which HTTP headers are supported.
+
+
 ## Development
 
 - [x] Create a UI with list of configs, current version,
@@ -69,13 +80,14 @@
       update zip file
 - [x] Add `UpdateConfig` for working with json config files
 - [x] Add applying non-streaming update
-- [ ] Prepare streaming update (partially downloading package)
-- [ ] Add applying streaming update
+- [x] Prepare streaming update (partially downloading package)
+- [x] Add applying streaming update
+- [x] Add stop/reset the update
+- [x] Add demo for passing HTTP headers to `UpdateEngine#applyPayload`
+- [x] [Package compatibility check](https://source.android.com/devices/architecture/vintf/match-rules)
 - [ ] Add tests for `MainActivity`
-- [ ] Add stop/reset the update
-- [ ] Verify system partition checksum for package
-- [ ] HAL compatibility check
 - [ ] Change partition demo
+- [ ] Verify system partition checksum for package
 - [ ] Add non-A/B updates demo
 
 
diff --git a/updater_sample/res/raw/sample.json b/updater_sample/res/raw/sample.json
index b6f4cdc..46fbfa3 100644
--- a/updater_sample/res/raw/sample.json
+++ b/updater_sample/res/raw/sample.json
@@ -1,13 +1,14 @@
 {
     "__name": "name will be visible on UI",
     "__url": "https:// or file:// uri to update package (zip, xz, ...)",
-    "__type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
+    "__ab_install_type": "NON_STREAMING (from a local file) OR STREAMING (on the fly)",
     "name": "SAMPLE-cake-release BUILD-12345",
     "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",
+        "__authorization": "it will be sent to OTA package server as value of HTTP header - Authorization",
         "property_files": [
             {
                 "__filename": "name of the file in package",
@@ -17,6 +18,7 @@
                 "offset": 531,
                 "size": 5012323
             }
-        ]
+        ],
+        "authorization": "Basic my-secret-token"
     }
 }
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
index 23510e4..9bdd8b9 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateConfig.java
@@ -25,6 +25,7 @@
 
 import java.io.File;
 import java.io.Serializable;
+import java.util.Optional;
 
 /**
  * An update description. It will be parsed from JSON, which is intended to
@@ -69,16 +70,22 @@
         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()];
+            PackageFile[] propertyFiles =
+                new PackageFile[propertyFilesJson.length()];
             for (int i = 0; i < propertyFilesJson.length(); i++) {
                 JSONObject p = propertyFilesJson.getJSONObject(i);
-                propertyFiles[i] = new InnerFile(
+                propertyFiles[i] = new PackageFile(
                         p.getString("filename"),
                         p.getLong("offset"),
                         p.getLong("size"));
             }
-            c.mAbStreamingMetadata = new StreamingMetadata(propertyFiles);
+            String authorization = null;
+            if (meta.has("authorization")) {
+                authorization = meta.getString("authorization");
+            }
+            c.mAbStreamingMetadata = new StreamingMetadata(
+                    propertyFiles,
+                    authorization);
         }
         c.mRawJson = json;
         return c;
@@ -176,25 +183,31 @@
         private static final long serialVersionUID = 31042L;
 
         /** defines beginning of update data in archive */
-        private InnerFile[] mPropertyFiles;
+        private PackageFile[] mPropertyFiles;
 
-        public StreamingMetadata() {
-            mPropertyFiles = new InnerFile[0];
-        }
+        /** SystemUpdaterSample receives the authorization token from the OTA server, in addition
+         * to the package URL. It passes on the info to update_engine, so that the latter can
+         * fetch the data from the package server directly with the token. */
+        private String mAuthorization;
 
-        public StreamingMetadata(InnerFile[] propertyFiles) {
+        public StreamingMetadata(PackageFile[] propertyFiles, String authorization) {
             this.mPropertyFiles = propertyFiles;
+            this.mAuthorization = authorization;
         }
 
-        public InnerFile[] getPropertyFiles() {
+        public PackageFile[] getPropertyFiles() {
             return mPropertyFiles;
         }
+
+        public Optional<String> getAuthorization() {
+            return mAuthorization == null ? Optional.empty() : Optional.of(mAuthorization);
+        }
     }
 
     /**
      * Description of a file in an OTA package zip file.
      */
-    public static class InnerFile implements Serializable {
+    public static class PackageFile implements Serializable {
 
         private static final long serialVersionUID = 31043L;
 
@@ -207,7 +220,7 @@
         /** size of the update data in archive */
         private long mSize;
 
-        public InnerFile(String filename, long offset, long size) {
+        public PackageFile(String filename, long offset, long size) {
             this.mFilename = filename;
             this.mOffset = offset;
             this.mSize = size;
@@ -224,7 +237,6 @@
         public long getSize() {
             return mSize;
         }
-
     }
 
 }
diff --git a/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
new file mode 100644
index 0000000..222bb0a
--- /dev/null
+++ b/updater_sample/src/com/example/android/systemupdatersample/services/PrepareStreamingService.java
@@ -0,0 +1,249 @@
+/*
+ * 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.services;
+
+import static com.example.android.systemupdatersample.util.PackageFiles.COMPATIBILITY_ZIP_FILE_NAME;
+import static com.example.android.systemupdatersample.util.PackageFiles.OTA_PACKAGE_DIR;
+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 android.app.IntentService;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.RecoverySystem;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.util.FileDownloader;
+import com.example.android.systemupdatersample.util.PackageFiles;
+import com.example.android.systemupdatersample.util.PayloadSpecs;
+import com.example.android.systemupdatersample.util.UpdateConfigs;
+import com.google.common.collect.ImmutableSet;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Optional;
+
+/**
+ * This IntentService will download/extract the necessary files from the package zip
+ * without downloading the whole package. And it constructs {@link PayloadSpec}.
+ * All this work required to install streaming A/B updates.
+ *
+ * PrepareStreamingService runs on it's own thread. It will notify activity
+ * using interface {@link UpdateResultCallback} when update is ready to install.
+ */
+public class PrepareStreamingService extends IntentService {
+
+    /**
+     * UpdateResultCallback result codes.
+     */
+    public static final int RESULT_CODE_SUCCESS = 0;
+    public static final int RESULT_CODE_ERROR = 1;
+
+    /**
+     * This interface is used to send results from {@link PrepareStreamingService} to
+     * {@code MainActivity}.
+     */
+    public interface UpdateResultCallback {
+
+        /**
+         * Invoked when files are downloaded and payload spec is constructed.
+         *
+         * @param resultCode result code, values are defined in {@link PrepareStreamingService}
+         * @param payloadSpec prepared payload spec for streaming update
+         */
+        void onReceiveResult(int resultCode, PayloadSpec payloadSpec);
+    }
+
+    /**
+     * Starts PrepareStreamingService.
+     *
+     * @param context application context
+     * @param config update config
+     * @param resultCallback callback that will be called when the update is ready to be installed
+     */
+    public static void startService(Context context,
+            UpdateConfig config,
+            UpdateResultCallback resultCallback) {
+        Log.d(TAG, "Starting PrepareStreamingService");
+        ResultReceiver receiver = new CallbackResultReceiver(new Handler(), resultCallback);
+        Intent intent = new Intent(context, PrepareStreamingService.class);
+        intent.putExtra(EXTRA_PARAM_CONFIG, config);
+        intent.putExtra(EXTRA_PARAM_RESULT_RECEIVER, receiver);
+        context.startService(intent);
+    }
+
+    public PrepareStreamingService() {
+        super(TAG);
+    }
+
+    private static final String TAG = "PrepareStreamingService";
+
+    /**
+     * Extra params that will be sent from Activity to IntentService.
+     */
+    private static final String EXTRA_PARAM_CONFIG = "config";
+    private static final String EXTRA_PARAM_RESULT_RECEIVER = "result-receiver";
+
+    /**
+     * The files that should be downloaded before streaming.
+     */
+    private static final ImmutableSet<String> PRE_STREAMING_FILES_SET =
+            ImmutableSet.of(
+                PackageFiles.CARE_MAP_FILE_NAME,
+                PackageFiles.COMPATIBILITY_ZIP_FILE_NAME,
+                PackageFiles.METADATA_FILE_NAME,
+                PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME
+            );
+
+    @Override
+    protected void onHandleIntent(Intent intent) {
+        Log.d(TAG, "On handle intent is called");
+        UpdateConfig config = intent.getParcelableExtra(EXTRA_PARAM_CONFIG);
+        ResultReceiver resultReceiver = intent.getParcelableExtra(EXTRA_PARAM_RESULT_RECEIVER);
+
+        try {
+            PayloadSpec spec = execute(config);
+            resultReceiver.send(RESULT_CODE_SUCCESS, CallbackResultReceiver.createBundle(spec));
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to prepare streaming update", e);
+            resultReceiver.send(RESULT_CODE_ERROR, null);
+        }
+    }
+
+    /**
+     * 1. Downloads files for streaming updates.
+     * 2. Makes sure required files are present.
+     * 3. Checks OTA package compatibility with the device.
+     * 4. Constructs {@link PayloadSpec} for streaming update.
+     */
+    private static PayloadSpec execute(UpdateConfig config)
+            throws IOException, PreparationFailedException {
+
+        downloadPreStreamingFiles(config, OTA_PACKAGE_DIR);
+
+        Optional<UpdateConfig.PackageFile> payloadBinary =
+                UpdateConfigs.getPropertyFile(PAYLOAD_BINARY_FILE_NAME, config);
+
+        if (!payloadBinary.isPresent()) {
+            throw new PreparationFailedException(
+                    "Failed to find " + PAYLOAD_BINARY_FILE_NAME + " in config");
+        }
+
+        if (!UpdateConfigs.getPropertyFile(PAYLOAD_PROPERTIES_FILE_NAME, config).isPresent()
+                || !Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile().exists()) {
+            throw new IOException(PAYLOAD_PROPERTIES_FILE_NAME + " not found");
+        }
+
+        File compatibilityFile = Paths.get(OTA_PACKAGE_DIR, COMPATIBILITY_ZIP_FILE_NAME).toFile();
+        if (compatibilityFile.isFile()) {
+            Log.i(TAG, "Verifying OTA package for compatibility with the device");
+            if (!verifyPackageCompatibility(compatibilityFile)) {
+                throw new PreparationFailedException(
+                        "OTA package is not compatible with this device");
+            }
+        }
+
+        return PayloadSpecs.forStreaming(config.getUrl(),
+                payloadBinary.get().getOffset(),
+                payloadBinary.get().getSize(),
+                Paths.get(OTA_PACKAGE_DIR, PAYLOAD_PROPERTIES_FILE_NAME).toFile());
+    }
+
+    /**
+     * Downloads files defined in {@link UpdateConfig#getStreamingMetadata()}
+     * and exists in {@code PRE_STREAMING_FILES_SET}, and put them
+     * in directory {@code dir}.
+     * @throws IOException when can't download a file
+     */
+    private static void downloadPreStreamingFiles(UpdateConfig config, String dir)
+            throws IOException {
+        Log.d(TAG, "Deleting existing files from " + dir);
+        for (String file : PRE_STREAMING_FILES_SET) {
+            Files.deleteIfExists(Paths.get(OTA_PACKAGE_DIR, file));
+        }
+        Log.d(TAG, "Downloading files to " + dir);
+        for (UpdateConfig.PackageFile file : config.getStreamingMetadata().getPropertyFiles()) {
+            if (PRE_STREAMING_FILES_SET.contains(file.getFilename())) {
+                Log.d(TAG, "Downloading file " + file.getFilename());
+                FileDownloader downloader = new FileDownloader(
+                        config.getUrl(),
+                        file.getOffset(),
+                        file.getSize(),
+                        Paths.get(dir, file.getFilename()).toFile());
+                downloader.download();
+            }
+        }
+    }
+
+    /**
+     * @param file physical location of {@link PackageFiles#COMPATIBILITY_ZIP_FILE_NAME}
+     * @return true if OTA package is compatible with this device
+     */
+    private static boolean verifyPackageCompatibility(File file) {
+        try {
+            return RecoverySystem.verifyPackageCompatibility(file);
+        } catch (IOException e) {
+            Log.e(TAG, "Failed to verify package compatibility", e);
+            return false;
+        }
+    }
+
+    /**
+     * Used by {@link PrepareStreamingService} to pass {@link PayloadSpec}
+     * to {@link UpdateResultCallback#onReceiveResult}.
+     */
+    private static class CallbackResultReceiver extends ResultReceiver {
+
+        static Bundle createBundle(PayloadSpec payloadSpec) {
+            Bundle b = new Bundle();
+            b.putSerializable(BUNDLE_PARAM_PAYLOAD_SPEC, payloadSpec);
+            return b;
+        }
+
+        private static final String BUNDLE_PARAM_PAYLOAD_SPEC = "payload-spec";
+
+        private UpdateResultCallback mUpdateResultCallback;
+
+        CallbackResultReceiver(Handler handler, UpdateResultCallback updateResultCallback) {
+            super(handler);
+            this.mUpdateResultCallback = updateResultCallback;
+        }
+
+        @Override
+        protected void onReceiveResult(int resultCode, Bundle resultData) {
+            PayloadSpec payloadSpec = null;
+            if (resultCode == RESULT_CODE_SUCCESS) {
+                payloadSpec = (PayloadSpec) resultData.getSerializable(BUNDLE_PARAM_PAYLOAD_SPEC);
+            }
+            mUpdateResultCallback.onReceiveResult(resultCode, payloadSpec);
+        }
+    }
+
+    private static class PreparationFailedException extends Exception {
+        PreparationFailedException(String message) {
+            super(message);
+        }
+    }
+
+}
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 d6a6ce3..1708256 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -34,12 +34,14 @@
 import com.example.android.systemupdatersample.PayloadSpec;
 import com.example.android.systemupdatersample.R;
 import com.example.android.systemupdatersample.UpdateConfig;
+import com.example.android.systemupdatersample.services.PrepareStreamingService;
 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.ArrayList;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -50,6 +52,10 @@
 
     private static final String TAG = "MainActivity";
 
+    /** HTTP Header: User-Agent; it will be sent to the server when streaming the payload. */
+    private static final String HTTP_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
+            + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36";
+
     private TextView mTextViewBuild;
     private Spinner mSpinnerConfigs;
     private TextView mTextViewConfigsDirHint;
@@ -294,9 +300,25 @@
                         .show();
                 return;
             }
-            updateEngineApplyPayload(payload);
+            updateEngineApplyPayload(payload, null);
         } else {
             Log.d(TAG, "Starting PrepareStreamingService");
+            PrepareStreamingService.startService(this, config, (code, payloadSpec) -> {
+                if (code == PrepareStreamingService.RESULT_CODE_SUCCESS) {
+                    List<String> extraProperties = new ArrayList<>();
+                    extraProperties.add("USER_AGENT=" + HTTP_USER_AGENT);
+                    config.getStreamingMetadata()
+                            .getAuthorization()
+                            .ifPresent(s -> extraProperties.add("AUTHORIZATION=" + s));
+                    updateEngineApplyPayload(payloadSpec, extraProperties);
+                } else {
+                    Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
+                    Toast.makeText(
+                            MainActivity.this,
+                            "PrepareStreamingService failed, result code is " + code,
+                            Toast.LENGTH_LONG).show();
+                }
+            });
         }
     }
 
@@ -305,14 +327,21 @@
      *
      * UpdateEngine works asynchronously. This method doesn't wait until
      * end of the update.
+     *
+     * @param payloadSpec contains url, offset and size to {@code PAYLOAD_BINARY_FILE_NAME}
+     * @param extraProperties additional properties to pass to {@link UpdateEngine#applyPayload}
      */
-    private void updateEngineApplyPayload(PayloadSpec payloadSpec) {
+    private void updateEngineApplyPayload(PayloadSpec payloadSpec, List<String> extraProperties) {
+        ArrayList<String> properties = new ArrayList<>(payloadSpec.getProperties());
+        if (extraProperties != null) {
+            properties.addAll(extraProperties);
+        }
         try {
             mUpdateEngine.applyPayload(
                     payloadSpec.getUrl(),
                     payloadSpec.getOffset(),
                     payloadSpec.getSize(),
-                    payloadSpec.getProperties().toArray(new String[0]));
+                    properties.toArray(new String[0]));
         } catch (Exception e) {
             Log.e(TAG, "UpdateEngine failed to apply the update", e);
             Toast.makeText(
diff --git a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
index 5c1d711..ddd0919 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/FileDownloader.java
@@ -38,22 +38,23 @@
     private String mUrl;
     private long mOffset;
     private long mSize;
-    private File mOut;
+    private File mDestination;
 
-    public FileDownloader(String url, long offset, long size, File out) {
+    public FileDownloader(String url, long offset, long size, File destination) {
         this.mUrl = url;
         this.mOffset = offset;
         this.mSize = size;
-        this.mOut = out;
+        this.mDestination = destination;
     }
 
     /**
      * Downloads the file with given offset and size.
+     * @throws IOException when can't download the file
      */
     public void download() throws IOException {
-        Log.d("FileDownloader", "downloading " + mOut.getName()
+        Log.d("FileDownloader", "downloading " + mDestination.getName()
                 + " from " + mUrl
-                + " to " + mOut.getAbsolutePath());
+                + " to " + mDestination.getAbsolutePath());
 
         URL url = new URL(mUrl);
         URLConnection connection = url.openConnection();
@@ -61,7 +62,7 @@
 
         // download the file
         try (InputStream input = connection.getInputStream()) {
-            try (OutputStream output = new FileOutputStream(mOut)) {
+            try (OutputStream output = new FileOutputStream(mDestination)) {
                 long skipped = input.skip(mOffset);
                 if (skipped != mOffset) {
                     throw new IOException("Can't download file "
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 71d4df8..5080cb6 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/util/UpdateConfigs.java
@@ -26,14 +26,16 @@
 import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 
 /**
  * Utility class for working with json update configurations.
  */
 public final class UpdateConfigs {
 
-    private static final String UPDATE_CONFIGS_ROOT = "configs/";
+    public static final String UPDATE_CONFIGS_ROOT = "configs/";
 
     /**
      * @param configs update configs
@@ -48,13 +50,12 @@
      * @return configs root directory
      */
     public static String getConfigsRoot(Context context) {
-        return Paths.get(context.getFilesDir().toString(),
-                UPDATE_CONFIGS_ROOT).toString();
+        return Paths
+                .get(context.getFilesDir().toString(), UPDATE_CONFIGS_ROOT)
+                .toString();
     }
 
     /**
-     * It parses only {@code .json} files.
-     *
      * @param context application context
      * @return list of configs from directory {@link UpdateConfigs#getConfigsRoot}
      */
@@ -80,5 +81,20 @@
         return configs;
     }
 
+    /**
+     * @param filename searches by given filename
+     * @param config searches in {@link UpdateConfig#getStreamingMetadata()}
+     * @return offset and size of {@code filename} in the package zip file
+     *         stored as {@link UpdateConfig.PackageFile}.
+     */
+    public static Optional<UpdateConfig.PackageFile> getPropertyFile(
+            final String filename,
+            UpdateConfig config) {
+        return Arrays
+                .stream(config.getStreamingMetadata().getPropertyFiles())
+                .filter(file -> filename.equals(file.getFilename()))
+                .findFirst();
+    }
+
     private UpdateConfigs() {}
 }
diff --git a/updater_sample/tests/res/raw/ota_002_package.zip b/updater_sample/tests/res/raw/ota_002_package.zip
index 145c62e..6bf2a23 100644
--- a/updater_sample/tests/res/raw/ota_002_package.zip
+++ b/updater_sample/tests/res/raw/ota_002_package.zip
Binary files differ
diff --git a/updater_sample/tests/res/raw/update_config_stream_002.json b/updater_sample/tests/res/raw/update_config_stream_002.json
index f00f19c..cf4469b 100644
--- a/updater_sample/tests/res/raw/update_config_stream_002.json
+++ b/updater_sample/tests/res/raw/update_config_stream_002.json
@@ -4,29 +4,34 @@
     "ab_streaming_metadata": {
         "property_files": [
             {
+                "filename": "payload_metadata.bin",
+                "offset": 41,
+                "size": 827
+            },
+            {
                 "filename": "payload.bin",
                 "offset": 41,
-                "size": 7
+                "size": 1392
             },
             {
                 "filename": "payload_properties.txt",
-                "offset": 100,
-                "size": 18
+                "offset": 1485,
+                "size": 147
             },
             {
                 "filename": "care_map.txt",
-                "offset": 160,
-                "size": 8
+                "offset": 1674,
+                "size": 12
             },
             {
                 "filename": "compatibility.zip",
-                "offset": 215,
-                "size": 13
+                "offset": 1733,
+                "size": 17
             },
             {
                 "filename": "metadata",
-                "offset": 287,
-                "size": 8
+                "offset": 1809,
+                "size": 29
             }
         ]
     },
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
index 80506ee..a136ff0 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/util/FileDownloaderTest.java
@@ -16,7 +16,7 @@
 
 package com.example.android.systemupdatersample.util;
 
-import static junit.framework.Assert.assertEquals;
+import static org.junit.Assert.assertEquals;
 
 import android.content.Context;
 import android.support.test.InstrumentationRegistry;
@@ -70,11 +70,11 @@
                 .toFile();
         Files.deleteIfExists(outFile.toPath());
         // download a chunk of ota.zip
-        FileDownloader downloader = new FileDownloader(url, 160, 8, outFile);
+        FileDownloader downloader = new FileDownloader(url, 1674, 12, outFile);
         downloader.download();
         String downloadedContent = String.join("\n", Files.readAllLines(outFile.toPath()));
         // archive contains text files with uppercase filenames
-        assertEquals("CARE_MAP", downloadedContent);
+        assertEquals("CARE_MAP-TXT", 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 2912e20..d9e5465 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
@@ -17,8 +17,6 @@
 package com.example.android.systemupdatersample.util;
 
 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;
@@ -29,6 +27,7 @@
 import android.support.test.runner.AndroidJUnit4;
 
 import com.example.android.systemupdatersample.PayloadSpec;
+import com.example.android.systemupdatersample.tests.R;
 import com.google.common.base.Charsets;
 import com.google.common.io.Files;
 
@@ -39,12 +38,8 @@
 import org.junit.runner.RunWith;
 
 import java.io.File;
-import java.io.FileOutputStream;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.util.zip.CRC32;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipOutputStream;
+import java.nio.file.Paths;
 
 /**
  * Tests if PayloadSpecs parses update package zip file correctly.
@@ -54,12 +49,11 @@
 public class PayloadSpecsTest {
 
     private static final String PROPERTIES_CONTENTS = "k1=val1\nkey2=val2";
-    private static final String PAYLOAD_CONTENTS    = "hello\nworld";
-    private static final int PAYLOAD_SIZE           = PAYLOAD_CONTENTS.length();
 
     private File mTestDir;
 
     private Context mTargetContext;
+    private Context mTestContext;
 
     @Rule
     public final ExpectedException thrown = ExpectedException.none();
@@ -67,21 +61,30 @@
     @Before
     public void setUp() {
         mTargetContext = InstrumentationRegistry.getTargetContext();
+        mTestContext = InstrumentationRegistry.getContext();
 
         mTestDir = mTargetContext.getFilesDir();
     }
 
     @Test
     public void forNonStreaming_works() throws Exception {
-        File packageFile = createMockZipFile();
+        // Prepare the target file
+        File packageFile = Paths
+                .get(mTargetContext.getCacheDir().getAbsolutePath(), "ota.zip")
+                .toFile();
+        java.nio.file.Files.deleteIfExists(packageFile.toPath());
+        java.nio.file.Files.copy(mTestContext.getResources().openRawResource(R.raw.ota_002_package),
+                packageFile.toPath());
         PayloadSpec spec = PayloadSpecs.forNonStreaming(packageFile);
 
         assertEquals("correct url", "file://" + packageFile.getAbsolutePath(), spec.getUrl());
         assertEquals("correct payload offset",
                 30 + PAYLOAD_BINARY_FILE_NAME.length(), spec.getOffset());
-        assertEquals("correct payload size", PAYLOAD_SIZE, spec.getSize());
-        assertArrayEquals("correct properties",
-                new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
+        assertEquals("correct payload size", 1392, spec.getSize());
+        assertEquals(4, spec.getProperties().size());
+        assertEquals(
+                "FILE_HASH=sEAK/NMbU7GGe01xt55FsPafIPk8IYyBOAd6SiDpiMs=",
+                spec.getProperties().get(0));
     }
 
     @Test
@@ -105,33 +108,6 @@
                 new String[]{"k1=val1", "key2=val2"}, spec.getProperties().toArray(new String[0]));
     }
 
-    /**
-     * Creates package zip file that contains payload.bin and payload_properties.txt
-     */
-    private File createMockZipFile() throws IOException {
-        File testFile = new File(mTestDir, "test.zip");
-        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(testFile))) {
-            // Add payload.bin entry.
-            ZipEntry entry = new ZipEntry(PAYLOAD_BINARY_FILE_NAME);
-            entry.setMethod(ZipEntry.STORED);
-            entry.setCompressedSize(PAYLOAD_SIZE);
-            entry.setSize(PAYLOAD_SIZE);
-            CRC32 crc = new CRC32();
-            crc.update(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
-            entry.setCrc(crc.getValue());
-            zos.putNextEntry(entry);
-            zos.write(PAYLOAD_CONTENTS.getBytes(StandardCharsets.UTF_8));
-            zos.closeEntry();
-
-            // Add payload properties entry.
-            ZipEntry propertiesEntry = new ZipEntry(PAYLOAD_PROPERTIES_FILE_NAME);
-            zos.putNextEntry(propertiesEntry);
-            zos.write(PROPERTIES_CONTENTS.getBytes(StandardCharsets.UTF_8));
-            zos.closeEntry();
-        }
-        return testFile;
-    }
-
     private File createMockPropertiesFile() throws IOException {
         File propertiesFile = new File(mTestDir, PackageFiles.PAYLOAD_PROPERTIES_FILE_NAME);
         Files.asCharSink(propertiesFile, Charsets.UTF_8).write(PROPERTIES_CONTENTS);
diff --git a/updater_sample/tools/gen_update_config.py b/updater_sample/tools/gen_update_config.py
index 0578124..4efa9f1 100755
--- a/updater_sample/tools/gen_update_config.py
+++ b/updater_sample/tools/gen_update_config.py
@@ -17,11 +17,13 @@
 """
 Given a OTA package file, produces update config JSON file.
 
-Example:  tools/gen_update_config.py \\
-            --ab_install_type=STREAMING \\
-            ota-build-001.zip  \\
-            my-config-001.json \\
-            http://foo.bar/ota-builds/ota-build-001.zip
+Example:
+      $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+            bootable/recovery/updater_sample/tools/gen_update_config.py \\
+                --ab_install_type=STREAMING \\
+                ota-build-001.zip  \\
+                my-config-001.json \\
+                http://foo.bar/ota-builds/ota-build-001.zip
 """
 
 import argparse
@@ -30,6 +32,8 @@
 import sys
 import zipfile
 
+import ota_from_target_files  # pylint: disable=import-error
+
 
 class GenUpdateConfig(object):
     """
@@ -41,7 +45,6 @@
 
     AB_INSTALL_TYPE_STREAMING = 'STREAMING'
     AB_INSTALL_TYPE_NON_STREAMING = 'NON_STREAMING'
-    METADATA_NAME = 'META-INF/com/android/metadata'
 
     def __init__(self, package, url, ab_install_type):
         self.package = package
@@ -82,37 +85,27 @@
     def _gen_ab_streaming_metadata(self):
         """Builds metadata for files required for streaming update."""
         with zipfile.ZipFile(self.package, 'r') as package_zip:
-            property_files = self._get_property_files(package_zip)
-
             metadata = {
-                'property_files': property_files
+                'property_files': self._get_property_files(package_zip)
             }
 
         return metadata
 
-    def _get_property_files(self, zip_file):
+    @staticmethod
+    def _get_property_files(package_zip):
         """Constructs the property-files list for A/B streaming metadata."""
 
-        def compute_entry_offset_size(name):
-            """Computes the zip entry offset and size."""
-            info = zip_file.getinfo(name)
-            offset = info.header_offset + len(info.FileHeader())
-            size = info.file_size
-            return {
-                'filename': os.path.basename(name),
-                'offset': offset,
-                'size': size,
-            }
-
+        ab_ota = ota_from_target_files.AbOtaPropertyFiles()
+        property_str = ab_ota.GetPropertyFilesString(package_zip, False)
         property_files = []
-        for entry in self.streaming_required:
-            property_files.append(compute_entry_offset_size(entry))
-        for entry in self.streaming_optional:
-            if entry in zip_file.namelist():
-                property_files.append(compute_entry_offset_size(entry))
-
-        # 'META-INF/com/android/metadata' is required
-        property_files.append(compute_entry_offset_size(GenUpdateConfig.METADATA_NAME))
+        for file in property_str.split(','):
+            filename, offset, size = file.split(':')
+            inner_file = {
+                'filename': filename,
+                'offset': int(offset),
+                'size': int(size)
+            }
+            property_files.append(inner_file)
 
         return property_files
 
diff --git a/updater_sample/tools/gen_update_config_test.py b/updater_sample/tools/test_gen_update_config.py
similarity index 69%
rename from updater_sample/tools/gen_update_config_test.py
rename to updater_sample/tools/test_gen_update_config.py
index 951d4c4..c907cf2 100755
--- a/updater_sample/tools/gen_update_config_test.py
+++ b/updater_sample/tools/test_gen_update_config.py
@@ -15,7 +15,11 @@
 # limitations under the License.
 
 """
-Tests gen_update_config.py
+Tests gen_update_config.py.
+
+Example:
+    $ PYTHONPATH=$ANDROID_BUILD_TOP/build/make/tools/releasetools:$PYTHONPATH \\
+        python3 -m unittest test_gen_update_config
 """
 
 import os.path
@@ -29,15 +33,21 @@
         """tests if streaming property files' offset and size are generated properly"""
         config, package = self._generate_config()
         property_files = config['ab_streaming_metadata']['property_files']
-        self.assertEqual(len(property_files), 5)
+        self.assertEqual(len(property_files), 6)
         with open(package, 'rb') as pkg_file:
             for prop in property_files:
                 filename, offset, size = prop['filename'], prop['offset'], prop['size']
                 pkg_file.seek(offset)
-                data = pkg_file.read(size).decode('ascii')
-                # data in the archive are just uppercase filenames without extension
-                expected_data = filename.split('.')[0].upper()
-                self.assertEqual(data, expected_data)
+                raw_data = pkg_file.read(size)
+                if filename in ['payload.bin', 'payload_metadata.bin']:
+                    pass
+                elif filename == 'payload_properties.txt':
+                    pass
+                elif filename == 'metadata':
+                    self.assertEqual(raw_data.decode('ascii'), 'META-INF/COM/ANDROID/METADATA')
+                else:
+                    expected_data = filename.replace('.', '-').upper()
+                    self.assertEqual(raw_data.decode('ascii'), expected_data)
 
     @staticmethod
     def _generate_config():
@@ -49,7 +59,3 @@
                               GenUpdateConfig.AB_INSTALL_TYPE_STREAMING)
         gen.run()
         return gen.config, ota_package
-
-
-if __name__ == '__main__':
-    unittest.main()