Merge "minui: Handle the failures from the drm backend in gr_init"
diff --git a/screen_ui.cpp b/screen_ui.cpp
index b9aba80..b4ef054 100644
--- a/screen_ui.cpp
+++ b/screen_ui.cpp
@@ -173,7 +173,9 @@
 
 ScreenRecoveryUI::~ScreenRecoveryUI() {
   progress_thread_stopped_ = true;
-  progress_thread_.join();
+  if (progress_thread_.joinable()) {
+    progress_thread_.join();
+  }
 }
 
 GRSurface* ScreenRecoveryUI::GetCurrentFrame() const {
diff --git a/tests/component/updater_test.cpp b/tests/component/updater_test.cpp
index de8fafd..6f3a3a2 100644
--- a/tests/component/updater_test.cpp
+++ b/tests/component/updater_test.cpp
@@ -828,3 +828,230 @@
   RunBlockImageUpdate(true, entries, image_file_, "t");
   ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
 }
+
+class ResumableUpdaterTest : public testing::TestWithParam<size_t> {
+ protected:
+  void SetUp() override {
+    RegisterBuiltins();
+    RegisterInstallFunctions();
+    RegisterBlockImageFunctions();
+
+    Paths::Get().set_cache_temp_source(temp_saved_source_.path);
+    Paths::Get().set_last_command_file(temp_last_command_.path);
+    Paths::Get().set_stash_directory_base(temp_stash_base_.path);
+
+    index_ = GetParam();
+    image_file_ = image_temp_file_.path;
+    last_command_file_ = temp_last_command_.path;
+  }
+
+  void TearDown() override {
+    // Clean up the last_command_file if any.
+    ASSERT_TRUE(android::base::RemoveFileIfExists(last_command_file_));
+
+    // Clear partition updated marker if any.
+    std::string updated_marker{ temp_stash_base_.path };
+    updated_marker += "/" + get_sha1(image_temp_file_.path) + ".UPDATED";
+    ASSERT_TRUE(android::base::RemoveFileIfExists(updated_marker));
+  }
+
+  TemporaryFile temp_saved_source_;
+  TemporaryDir temp_stash_base_;
+  std::string last_command_file_;
+  std::string image_file_;
+  size_t index_;
+
+ private:
+  TemporaryFile temp_last_command_;
+  TemporaryFile image_temp_file_;
+};
+
+static std::string g_source_image;
+static std::string g_target_image;
+static PackageEntries g_entries;
+
+static std::vector<std::string> GenerateTransferList() {
+  std::string a(4096, 'a');
+  std::string b(4096, 'b');
+  std::string c(4096, 'c');
+  std::string d(4096, 'd');
+  std::string e(4096, 'e');
+  std::string f(4096, 'f');
+  std::string g(4096, 'g');
+  std::string h(4096, 'h');
+  std::string i(4096, 'i');
+  std::string zero(4096, '\0');
+
+  std::string a_hash = get_sha1(a);
+  std::string b_hash = get_sha1(b);
+  std::string c_hash = get_sha1(c);
+  std::string e_hash = get_sha1(e);
+
+  auto loc = [](const std::string& range_text) {
+    std::vector<std::string> pieces = android::base::Split(range_text, "-");
+    size_t left;
+    size_t right;
+    if (pieces.size() == 1) {
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      right = left + 1;
+    } else {
+      CHECK_EQ(2u, pieces.size());
+      CHECK(android::base::ParseUint(pieces[0], &left));
+      CHECK(android::base::ParseUint(pieces[1], &right));
+      right++;
+    }
+    return android::base::StringPrintf("2,%zu,%zu", left, right);
+  };
+
+  // patch 1: "b d c" -> "g"
+  TemporaryFile patch_file_bdc_g;
+  std::string bdc = b + d + c;
+  std::string bdc_hash = get_sha1(bdc);
+  std::string g_hash = get_sha1(g);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(bdc.data()), bdc.size(),
+                             reinterpret_cast<const uint8_t*>(g.data()), g.size(),
+                             patch_file_bdc_g.path, nullptr));
+  std::string patch_bdc_g;
+  CHECK(android::base::ReadFileToString(patch_file_bdc_g.path, &patch_bdc_g));
+
+  // patch 2: "a b c d" -> "d c b"
+  TemporaryFile patch_file_abcd_dcb;
+  std::string abcd = a + b + c + d;
+  std::string abcd_hash = get_sha1(abcd);
+  std::string dcb = d + c + b;
+  std::string dcb_hash = get_sha1(dcb);
+  CHECK_EQ(0, bsdiff::bsdiff(reinterpret_cast<const uint8_t*>(abcd.data()), abcd.size(),
+                             reinterpret_cast<const uint8_t*>(dcb.data()), dcb.size(),
+                             patch_file_abcd_dcb.path, nullptr));
+  std::string patch_abcd_dcb;
+  CHECK(android::base::ReadFileToString(patch_file_abcd_dcb.path, &patch_abcd_dcb));
+
+  std::vector<std::string> transfer_list{
+    "4",
+    "10",  // total blocks written
+    "2",   // maximum stash entries
+    "2",   // maximum number of stashed blocks
+
+    // a b c d e a b c d e
+    "stash " + b_hash + " " + loc("1"),
+    // a b c d e a b c d e    [b(1)]
+    "stash " + c_hash + " " + loc("2"),
+    // a b c d e a b c d e    [b(1)][c(2)]
+    "new " + loc("1-2"),
+    // a i h d e a b c d e    [b(1)][c(2)]
+    "zero " + loc("0"),
+    // 0 i h d e a b c d e    [b(1)][c(2)]
+
+    // bsdiff "b d c" (from stash, 3, stash) to get g(3)
+    android::base::StringPrintf(
+        "bsdiff 0 %zu %s %s %s 3 %s %s %s:%s %s:%s",
+        patch_bdc_g.size(),                  // patch start (0), patch length
+        bdc_hash.c_str(),                    // source hash
+        g_hash.c_str(),                      // target hash
+        loc("3").c_str(),                    // target range
+        loc("3").c_str(), loc("1").c_str(),  // load "d" from block 3, into buffer at offset 1
+        b_hash.c_str(), loc("0").c_str(),    // load "b" from stash, into buffer at offset 0
+        c_hash.c_str(), loc("2").c_str()),   // load "c" from stash, into buffer at offset 2
+
+    // 0 i h g e a b c d e    [b(1)][c(2)]
+    "free " + b_hash,
+    // 0 i h g e a b c d e    [c(2)]
+    "free " + a_hash,
+    // 0 i h g e a b c d e
+    "stash " + a_hash + " " + loc("5"),
+    // 0 i h g e a b c d e    [a(5)]
+    "move " + e_hash + " " + loc("5") + " 1 " + loc("4"),
+    // 0 i h g e e b c d e    [a(5)]
+
+    // bsdiff "a b c d" (from stash, 6-8) to "d c b" (6-8)
+    android::base::StringPrintf(  //
+        "bsdiff %zu %zu %s %s %s 4 %s %s %s:%s",
+        patch_bdc_g.size(),                          // patch start
+        patch_bdc_g.size() + patch_abcd_dcb.size(),  // patch length
+        abcd_hash.c_str(),                           // source hash
+        dcb_hash.c_str(),                            // target hash
+        loc("6-8").c_str(),                          // target range
+        loc("6-8").c_str(),                          // load "b c d" from blocks 6-8
+        loc("1-3").c_str(),                          //   into buffer at offset 1-3
+        a_hash.c_str(),                              // load "a" from stash
+        loc("0").c_str()),                           //   into buffer at offset 0
+
+    // 0 i h g e e d c b e    [a(5)]
+    "new " + loc("4"),
+    // 0 i h g f e d c b e    [a(5)]
+    "move " + a_hash + " " + loc("9") + " 1 - " + a_hash + ":" + loc("0"),
+    // 0 i h g f e d c b a    [a(5)]
+    "free " + a_hash,
+    // 0 i h g f e d c b a
+  };
+
+  std::string new_data = i + h + f;
+  std::string patch_data = patch_bdc_g + patch_abcd_dcb;
+
+  g_entries = {
+    { "new_data", new_data },
+    { "patch_data", patch_data },
+  };
+  g_source_image = a + b + c + d + e + a + b + c + d + e;
+  g_target_image = zero + i + h + g + f + e + d + c + b + a;
+
+  return transfer_list;
+}
+
+static const std::vector<std::string> g_transfer_list = GenerateTransferList();
+
+INSTANTIATE_TEST_CASE_P(InterruptAfterEachCommand, ResumableUpdaterTest,
+                        ::testing::Range(static_cast<size_t>(0),
+                                         g_transfer_list.size() - kTransferListHeaderLines));
+
+TEST_P(ResumableUpdaterTest, InterruptVerifyResume) {
+  ASSERT_TRUE(android::base::WriteStringToFile(g_source_image, image_file_));
+
+  LOG(INFO) << "Interrupting at line " << index_ << " ("
+            << g_transfer_list[kTransferListHeaderLines + index_] << ")";
+
+  std::vector<std::string> transfer_list_copy{ g_transfer_list };
+  transfer_list_copy[kTransferListHeaderLines + index_] = "fail";
+
+  g_entries["transfer_list"] = android::base::Join(transfer_list_copy, '\n');
+
+  // Run update that's expected to fail.
+  RunBlockImageUpdate(false, g_entries, image_file_, "");
+
+  std::string last_command_expected;
+
+  // Assert the last_command_file.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    last_command_expected =
+        std::to_string(index_ - 1) + "\n" + g_transfer_list[kTransferListHeaderLines + index_ - 1];
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  g_entries["transfer_list"] = android::base::Join(g_transfer_list, '\n');
+
+  // Resume the interrupted update, by doing verification first.
+  RunBlockImageUpdate(true, g_entries, image_file_, "t");
+
+  // last_command_file should remain intact.
+  if (index_ == 0) {
+    ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+  } else {
+    std::string last_command_actual;
+    ASSERT_TRUE(android::base::ReadFileToString(last_command_file_, &last_command_actual));
+    ASSERT_EQ(last_command_expected, last_command_actual);
+  }
+
+  // Resume the update.
+  RunBlockImageUpdate(false, g_entries, image_file_, "t");
+
+  // last_command_file should be gone after successful update.
+  ASSERT_EQ(-1, access(last_command_file_.c_str(), R_OK));
+
+  std::string updated_image_actual;
+  ASSERT_TRUE(android::base::ReadFileToString(image_file_, &updated_image_actual));
+  ASSERT_EQ(g_target_image, updated_image_actual);
+}
diff --git a/tests/unit/rangeset_test.cpp b/tests/unit/rangeset_test.cpp
index 7ae193e..fc72f2f 100644
--- a/tests/unit/rangeset_test.cpp
+++ b/tests/unit/rangeset_test.cpp
@@ -209,6 +209,7 @@
   ASSERT_EQ(static_cast<size_t>(6), rs.GetBlockNumber(5));
   ASSERT_EQ(static_cast<size_t>(9), rs.GetBlockNumber(8));
 
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
   // Out of bound.
   ASSERT_EXIT(rs.GetBlockNumber(9), ::testing::KilledBySignal(SIGABRT), "");
 }
@@ -284,6 +285,8 @@
 
   ASSERT_EQ(static_cast<size_t>(10), rs.GetOffsetInRangeSet(4106));
   ASSERT_EQ(static_cast<size_t>(40970), rs.GetOffsetInRangeSet(4096 * 16 + 10));
+
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
   // block#10 not in range.
   ASSERT_EXIT(rs.GetOffsetInRangeSet(40970), ::testing::KilledBySignal(SIGABRT), "");
 }
diff --git a/tests/unit/screen_ui_test.cpp b/tests/unit/screen_ui_test.cpp
index 2562307..a3dd2ad 100644
--- a/tests/unit/screen_ui_test.cpp
+++ b/tests/unit/screen_ui_test.cpp
@@ -293,6 +293,11 @@
   ASSERT_FALSE(ui_->WasTextEverVisible());
 }
 
+TEST_F(ScreenRecoveryUITest, dtor_NotCallingInit) {
+  ui_.reset();
+  ASSERT_FALSE(ui_);
+}
+
 TEST_F(ScreenRecoveryUITest, ShowText) {
   ASSERT_TRUE(ui_->Init(kTestLocale));
   ASSERT_FALSE(ui_->IsTextVisible());
@@ -408,5 +413,7 @@
   ASSERT_TRUE(ui_->Init(kTestLocale));
   TemporaryDir resource_dir;
   Paths::Get().set_resource_dir(resource_dir.path);
+
+  ::testing::FLAGS_gtest_death_test_style = "threadsafe";
   ASSERT_EXIT(ui_->RunLoadAnimation(), ::testing::KilledBySignal(SIGABRT), "");
 }
diff --git a/ui.cpp b/ui.cpp
index 51d7f12..6c91d01 100644
--- a/ui.cpp
+++ b/ui.cpp
@@ -78,7 +78,9 @@
 RecoveryUI::~RecoveryUI() {
   ev_exit();
   input_thread_stopped_ = true;
-  input_thread_.join();
+  if (input_thread_.joinable()) {
+    input_thread_.join();
+  }
 }
 
 void RecoveryUI::OnKeyDetected(int key_code) {
diff --git a/updater/blockimg.cpp b/updater/blockimg.cpp
index 4adb974..bdb6463 100644
--- a/updater/blockimg.cpp
+++ b/updater/blockimg.cpp
@@ -1498,6 +1498,7 @@
                                       const std::vector<std::unique_ptr<Expr>>& argv,
                                       const CommandMap& command_map, bool dryrun) {
   CommandParameters params = {};
+  stash_map.clear();
   params.canwrite = !dryrun;
 
   LOG(INFO) << "performing " << (dryrun ? "verification" : "update");
diff --git a/updater_sample/README.md b/updater_sample/README.md
index f6c63a7..8ec43d3 100644
--- a/updater_sample/README.md
+++ b/updater_sample/README.md
@@ -91,6 +91,31 @@
 parameters, and handles update completion properly using `onPayloadApplicationCompleted`
 callback. The second problem is solved by adding `PAUSED` updater state.
 
+
+## Sample App UI
+
+### Text fields
+
+- `Current Build:` - shows current active build.
+- `Updater state:` - SystemUpdaterSample app state.
+- `Engine status:` - last reported update_engine status.
+- `Engine error:` - last reported payload application error.
+
+### Buttons
+
+- `Reload` - reloads update configs from device storage.
+- `View config` - shows selected update config.
+- `Apply` - applies selected update config.
+- `Stop` - cancel running update, calls `UpdateEngine#cancel`.
+- `Reset` - reset update, calls `UpdateEngine#resetStatus`, can be called
+            only when update is not running.
+- `Suspend` - suspend running update, uses `UpdateEngine#cancel`.
+- `Resume` - resumes suspended update, uses `UpdateEngine#applyPayload`.
+- `Switch Slot` - if `ab_config.force_switch_slot` config set true,
+            this button will be enabled after payload is applied,
+            to switch A/B slot on next reboot.
+
+
 ## Sending HTTP headers from UpdateEngine
 
 Sometimes OTA package server might require some HTTP headers to be present,
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
index 2fe04bd..e4c0934 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdateManager.java
@@ -64,8 +64,8 @@
 
     private AtomicBoolean mManualSwitchSlotRequired = new AtomicBoolean(true);
 
-    /** Validate state only once when app binds to UpdateEngine. */
-    private AtomicBoolean mStateValidityEnsured = new AtomicBoolean(false);
+    /** Synchronize state with engine status only once when app binds to UpdateEngine. */
+    private AtomicBoolean mStateSynchronized = new AtomicBoolean(false);
 
     @GuardedBy("mLock")
     private UpdateData mLastUpdateData = null;
@@ -90,10 +90,12 @@
     }
 
     /**
-     * Binds to {@link UpdateEngine}.
+     * Binds to {@link UpdateEngine}. Invokes onStateChangeCallback if present.
      */
     public void bind() {
-        mStateValidityEnsured.set(false);
+        getOnStateChangeCallback().ifPresent(callback -> callback.accept(mUpdaterState.get()));
+
+        mStateSynchronized.set(false);
         this.mUpdateEngine.bind(mUpdateEngineCallback);
     }
 
@@ -104,11 +106,8 @@
         this.mUpdateEngine.unbind();
     }
 
-    /**
-     * @return a number from {@code 0.0} to {@code 1.0}.
-     */
-    public float getProgress() {
-        return (float) this.mProgress.get();
+    public int getUpdaterState() {
+        return mUpdaterState.get();
     }
 
     /**
@@ -202,18 +201,37 @@
      * Updates {@link this.mState} and if state is changed,
      * it also notifies {@link this.mOnStateChangeCallback}.
      */
-    private void setUpdaterState(int updaterState) {
+    private void setUpdaterState(int newUpdaterState)
+            throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "setUpdaterState invoked newState=" + newUpdaterState);
         int previousState = mUpdaterState.get();
+        mUpdaterState.set(newUpdaterState);
+        if (previousState != newUpdaterState) {
+            getOnStateChangeCallback().ifPresent(callback -> callback.accept(newUpdaterState));
+        }
+    }
+
+    /**
+     * Same as {@link this.setUpdaterState}. Logs the error if new state
+     * cannot be set.
+     */
+    private void setUpdaterStateSilent(int newUpdaterState) {
         try {
-            mUpdaterState.set(updaterState);
+            setUpdaterState(newUpdaterState);
         } catch (UpdaterState.InvalidTransitionException e) {
-            // Note: invalid state transitions should be handled properly,
-            //       but to make sample app simple, we just throw runtime exception.
-            throw new RuntimeException("Can't set state " + updaterState, e);
+            // Most likely UpdateEngine status and UpdaterSample state got de-synchronized.
+            // To make sample app simple, we don't handle it properly.
+            Log.e(TAG, "Failed to set updater state", e);
         }
-        if (previousState != updaterState) {
-            getOnStateChangeCallback().ifPresent(callback -> callback.accept(updaterState));
-        }
+    }
+
+    /**
+     * Creates new UpdaterState, assigns it to {@link this.mUpdaterState},
+     * and notifies callbacks.
+     */
+    private void initializeUpdateState(int state) {
+        this.mUpdaterState = new UpdaterState(state);
+        getOnStateChangeCallback().ifPresent(callback -> callback.accept(state));
     }
 
     /**
@@ -224,13 +242,10 @@
      * update engine would throw an error when the method is called, and the only way to
      * handle it is to catch the exception.</p>
      */
-    public void cancelRunningUpdate() {
-        try {
-            mUpdateEngine.cancel();
-            setUpdaterState(UpdaterState.IDLE);
-        } catch (Exception e) {
-            Log.w(TAG, "UpdateEngine failed to stop the ongoing update", e);
-        }
+    public synchronized void cancelRunningUpdate() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "cancelRunningUpdate invoked");
+        setUpdaterState(UpdaterState.IDLE);
+        mUpdateEngine.cancel();
     }
 
     /**
@@ -240,13 +255,10 @@
      * update engine would throw an error when the method is called, and the only way to
      * handle it is to catch the exception.</p>
      */
-    public void resetUpdate() {
-        try {
-            mUpdateEngine.resetStatus();
-            setUpdaterState(UpdaterState.IDLE);
-        } catch (Exception e) {
-            Log.w(TAG, "UpdateEngine failed to reset the update", e);
-        }
+    public synchronized void resetUpdate() throws UpdaterState.InvalidTransitionException {
+        Log.d(TAG, "resetUpdate invoked");
+        setUpdaterState(UpdaterState.IDLE);
+        mUpdateEngine.resetStatus();
     }
 
     /**
@@ -255,7 +267,8 @@
      * <p>UpdateEngine works asynchronously. This method doesn't wait until
      * end of the update.</p>
      */
-    public void applyUpdate(Context context, UpdateConfig config) {
+    public synchronized void applyUpdate(Context context, UpdateConfig config)
+            throws UpdaterState.InvalidTransitionException {
         mEngineErrorCode.set(UpdateEngineErrorCodes.UNKNOWN);
         setUpdaterState(UpdaterState.RUNNING);
 
@@ -277,7 +290,8 @@
         }
     }
 
-    private void applyAbNonStreamingUpdate(UpdateConfig config) {
+    private void applyAbNonStreamingUpdate(UpdateConfig config)
+            throws UpdaterState.InvalidTransitionException {
         UpdateData.Builder builder = UpdateData.builder()
                 .setExtraProperties(prepareExtraProperties(config));
 
@@ -306,7 +320,7 @@
                 updateEngineApplyPayload(builder.build());
             } else {
                 Log.e(TAG, "PrepareStreamingService failed, result code is " + code);
-                setUpdaterState(UpdaterState.ERROR);
+                setUpdaterStateSilent(UpdaterState.ERROR);
             }
         });
     }
@@ -333,6 +347,8 @@
      * with the given id.</p>
      */
     private void updateEngineApplyPayload(UpdateData update) {
+        Log.d(TAG, "updateEngineApplyPayload invoked with url " + update.mPayload.getUrl());
+
         synchronized (mLock) {
             mLastUpdateData = update;
         }
@@ -348,11 +364,15 @@
                     properties.toArray(new String[0]));
         } catch (Exception e) {
             Log.e(TAG, "UpdateEngine failed to apply the update", e);
-            setUpdaterState(UpdaterState.ERROR);
+            setUpdaterStateSilent(UpdaterState.ERROR);
         }
     }
 
+    /**
+     * Re-applies {@link this.mLastUpdateData} to update_engine.
+     */
     private void updateEngineReApplyPayload() {
+        Log.d(TAG, "updateEngineReApplyPayload invoked");
         UpdateData lastUpdate;
         synchronized (mLock) {
             // mLastPayloadSpec might be empty in some cases.
@@ -377,8 +397,14 @@
      * {@link UpdateEngine#applyPayload} might take several seconds to finish, and it will
      * invoke callbacks {@link this#onStatusUpdate} and {@link this#onPayloadApplicationComplete)}.
      */
-    public void setSwitchSlotOnReboot() {
+    public synchronized void setSwitchSlotOnReboot() {
         Log.d(TAG, "setSwitchSlotOnReboot invoked");
+
+        // When mManualSwitchSlotRequired set false, next time
+        // onApplicationPayloadComplete is called,
+        // it will set updater state to REBOOT_REQUIRED.
+        mManualSwitchSlotRequired.set(false);
+
         UpdateData.Builder builder;
         synchronized (mLock) {
             // To make sample app simple, we don't handle it.
@@ -396,65 +422,62 @@
     }
 
     /**
-     * Verifies if mUpdaterState matches mUpdateEngineStatus.
-     * If they don't match, runs applyPayload to trigger onPayloadApplicationComplete
-     * callback, which updates mUpdaterState.
+     * Synchronize UpdaterState with UpdateEngine status.
+     * Apply necessary UpdateEngine operation if status are out of sync.
+     *
+     * It's expected to be called once when sample app binds itself to UpdateEngine.
      */
-    private void ensureCorrectUpdaterState() {
-        // When mUpdaterState is one of IDLE, PAUSED, ERROR, SLOT_SWITCH_REQUIRED
-        //    then mUpdateEngineStatus must be IDLE.
-        // When mUpdaterState is RUNNING,
-        //    then mUpdateEngineStatus must not be IDLE or UPDATED_NEED_REBOOT.
-        // When mUpdaterState is REBOOT_REQUIRED,
-        //    then mUpdateEngineStatus must be UPDATED_NEED_REBOOT.
+    private void synchronizeUpdaterStateWithUpdateEngineStatus() {
+        Log.d(TAG, "synchronizeUpdaterStateWithUpdateEngineStatus is invoked.");
+
         int state = mUpdaterState.get();
-        int updateEngineStatus = mUpdateEngineStatus.get();
-        if (state == UpdaterState.IDLE
-                || state == UpdaterState.ERROR
-                || state == UpdaterState.PAUSED
-                || state == UpdaterState.SLOT_SWITCH_REQUIRED) {
-            ensureUpdateEngineStatusIdle(state, updateEngineStatus);
-        } else if (state == UpdaterState.RUNNING) {
-            ensureUpdateEngineStatusRunning(state, updateEngineStatus);
-        } else if (state == UpdaterState.REBOOT_REQUIRED) {
-            ensureUpdateEngineStatusReboot(state, updateEngineStatus);
-        }
-    }
+        int engineStatus = mUpdateEngineStatus.get();
 
-    private void ensureUpdateEngineStatusIdle(int state, int updateEngineStatus) {
-        if (updateEngineStatus == UpdateEngine.UpdateStatusConstants.IDLE) {
+        if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
+            // If update has been installed before running the sample app,
+            // set state to REBOOT_REQUIRED.
+            initializeUpdateState(UpdaterState.REBOOT_REQUIRED);
             return;
         }
-        // It might happen when update is started not from the sample app.
-        // To make the sample app simple, we won't handle this case.
-        throw new RuntimeException("When mUpdaterState is " + state
-                + " mUpdateEngineStatus expected to be "
-                + UpdateEngine.UpdateStatusConstants.IDLE
-                + ", but it is " + updateEngineStatus);
-    }
 
-    private void ensureUpdateEngineStatusRunning(int state, int updateEngineStatus) {
-        if (updateEngineStatus != UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
-                && updateEngineStatus != UpdateEngine.UpdateStatusConstants.IDLE) {
-            return;
+        switch (state) {
+            case UpdaterState.IDLE:
+            case UpdaterState.ERROR:
+            case UpdaterState.PAUSED:
+            case UpdaterState.SLOT_SWITCH_REQUIRED:
+                // It might happen when update is started not from the sample app.
+                // To make the sample app simple, we won't handle this case.
+                Preconditions.checkState(
+                        engineStatus == UpdateEngine.UpdateStatusConstants.IDLE,
+                        "When mUpdaterState is %s, mUpdateEngineStatus "
+                                + "must be 0/IDLE, but it is %s",
+                        state,
+                        engineStatus);
+                break;
+            case UpdaterState.RUNNING:
+                if (engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
+                        || engineStatus == UpdateEngine.UpdateStatusConstants.IDLE) {
+                    Log.i(TAG, "ensureUpdateEngineStatusIsRunning - re-applying last payload");
+                    // Re-apply latest update. It makes update_engine to invoke
+                    // onPayloadApplicationComplete callback. The callback notifies
+                    // if update was successful or not.
+                    updateEngineReApplyPayload();
+                }
+                break;
+            case UpdaterState.REBOOT_REQUIRED:
+                // This might happen when update is installed by other means,
+                // and sample app is not aware of it.
+                // To make the sample app simple, we won't handle this case.
+                Preconditions.checkState(
+                        engineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT,
+                        "When mUpdaterState is %s, mUpdateEngineStatus "
+                                + "must be 6/UPDATED_NEED_REBOOT, but it is %s",
+                        state,
+                        engineStatus);
+                break;
+            default:
+                throw new IllegalStateException("This block should not be reached.");
         }
-        // Re-apply latest update. It makes update_engine to invoke
-        // onPayloadApplicationComplete callback. The callback notifies
-        // if update was successful or not.
-        updateEngineReApplyPayload();
-    }
-
-    private void ensureUpdateEngineStatusReboot(int state, int updateEngineStatus) {
-        if (updateEngineStatus == UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT) {
-            return;
-        }
-        // This might happen when update is installed by other means,
-        // and sample app is not aware of it. To make the sample app simple,
-        // we won't handle this case.
-        throw new RuntimeException("When mUpdaterState is " + state
-                + " mUpdateEngineStatus expected to be "
-                + UpdateEngine.UpdateStatusConstants.UPDATED_NEED_REBOOT
-                + ", but it is " + updateEngineStatus);
     }
 
     /**
@@ -468,13 +491,19 @@
      * @param progress a number from 0.0 to 1.0.
      */
     private void onStatusUpdate(int status, float progress) {
+        Log.d(TAG, String.format(
+                        "onStatusUpdate invoked, status=%s, progress=%.2f",
+                        status,
+                        progress));
+
         int previousStatus = mUpdateEngineStatus.get();
         mUpdateEngineStatus.set(status);
         mProgress.set(progress);
 
-        if (!mStateValidityEnsured.getAndSet(true)) {
-            // We ensure correct state once only when sample app is bound to UpdateEngine.
-            ensureCorrectUpdaterState();
+        if (!mStateSynchronized.getAndSet(true)) {
+            // We synchronize state with engine status once
+            // only when sample app is bound to UpdateEngine.
+            synchronizeUpdaterStateWithUpdateEngineStatus();
         }
 
         getOnProgressUpdateCallback().ifPresent(callback -> callback.accept(progress));
@@ -489,11 +518,11 @@
         mEngineErrorCode.set(errorCode);
         if (errorCode == UpdateEngine.ErrorCodeConstants.SUCCESS
                 || errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
-            setUpdaterState(isManualSwitchSlotRequired()
+            setUpdaterStateSilent(isManualSwitchSlotRequired()
                     ? UpdaterState.SLOT_SWITCH_REQUIRED
                     : UpdaterState.REBOOT_REQUIRED);
         } else if (errorCode != UpdateEngineErrorCodes.USER_CANCELLED) {
-            setUpdaterState(UpdaterState.ERROR);
+            setUpdaterStateSilent(UpdaterState.ERROR);
         }
 
         getOnEngineCompleteCallback()
diff --git a/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
index 36a9098..573d336 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/UpdaterState.java
@@ -51,12 +51,15 @@
      * are allowed to transition to from key.
      */
     private static final ImmutableMap<Integer, ImmutableSet<Integer>> TRANSITIONS =
-            ImmutableMap.of(
-                    IDLE, ImmutableSet.of(RUNNING),
-                    RUNNING, ImmutableSet.of(ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED),
-                    PAUSED, ImmutableSet.of(RUNNING),
-                    SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR)
-            );
+            ImmutableMap.<Integer, ImmutableSet<Integer>>builder()
+                    .put(IDLE, ImmutableSet.of(ERROR, RUNNING))
+                    .put(RUNNING, ImmutableSet.of(
+                            ERROR, PAUSED, REBOOT_REQUIRED, SLOT_SWITCH_REQUIRED))
+                    .put(PAUSED, ImmutableSet.of(ERROR, RUNNING, IDLE))
+                    .put(SLOT_SWITCH_REQUIRED, ImmutableSet.of(ERROR, IDLE))
+                    .put(ERROR, ImmutableSet.of(IDLE))
+                    .put(REBOOT_REQUIRED, ImmutableSet.of(IDLE))
+                    .build();
 
     private AtomicInteger mState;
 
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 1de72c2..6c71cb6 100644
--- a/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
+++ b/updater_sample/src/com/example/android/systemupdatersample/ui/MainActivity.java
@@ -88,7 +88,7 @@
 
         this.mTextViewConfigsDirHint.setText(UpdateConfigs.getConfigsRoot(this));
 
-        uiReset();
+        uiResetWidgets();
         loadUpdateConfigs();
 
         this.mUpdateManager.setOnStateChangeCallback(this::onUpdaterStateChange);
@@ -108,7 +108,6 @@
     @Override
     protected void onResume() {
         super.onResume();
-        // TODO(zhomart) load saved states
         // Binding to UpdateEngine invokes onStatusUpdate callback,
         // persisted updater state has to be loaded and prepared beforehand.
         this.mUpdateManager.bind();
@@ -117,7 +116,6 @@
     @Override
     protected void onPause() {
         this.mUpdateManager.unbind();
-        // TODO(zhomart) save state
         super.onPause();
     }
 
@@ -149,14 +147,22 @@
                 .setMessage("Do you really want to apply this update?")
                 .setIcon(android.R.drawable.ic_dialog_alert)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
-                    uiSetUpdating();
+                    uiResetWidgets();
                     uiResetEngineText();
-                    mUpdateManager.applyUpdate(this, getSelectedConfig());
+                    applyUpdate(getSelectedConfig());
                 })
                 .setNegativeButton(android.R.string.cancel, null)
                 .show();
     }
 
+    private void applyUpdate(UpdateConfig config) {
+        try {
+            mUpdateManager.applyUpdate(this, config);
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to apply update " + config.getName(), e);
+        }
+    }
+
     /**
      * stop button clicked
      */
@@ -166,11 +172,19 @@
                 .setMessage("Do you really want to cancel running update?")
                 .setIcon(android.R.drawable.ic_dialog_alert)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
-                    mUpdateManager.cancelRunningUpdate();
+                    cancelRunningUpdate();
                 })
                 .setNegativeButton(android.R.string.cancel, null).show();
     }
 
+    private void cancelRunningUpdate() {
+        try {
+            mUpdateManager.cancelRunningUpdate();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to cancel running update", e);
+        }
+    }
+
     /**
      * reset button clicked
      */
@@ -181,11 +195,19 @@
                         + " and restore old version?")
                 .setIcon(android.R.drawable.ic_dialog_alert)
                 .setPositiveButton(android.R.string.ok, (dialog, whichButton) -> {
-                    mUpdateManager.resetUpdate();
+                    resetUpdate();
                 })
                 .setNegativeButton(android.R.string.cancel, null).show();
     }
 
+    private void resetUpdate() {
+        try {
+            mUpdateManager.resetUpdate();
+        } catch (UpdaterState.InvalidTransitionException e) {
+            Log.e(TAG, "Failed to reset update", e);
+        }
+    }
+
     /**
      * switch slot button clicked
      */
@@ -199,9 +221,25 @@
      * values from {@link UpdaterState}.
      */
     private void onUpdaterStateChange(int state) {
-        Log.i(TAG, "onUpdaterStateChange invoked state=" + state);
+        Log.i(TAG, "UpdaterStateChange state="
+                + UpdaterState.getStateText(state)
+                + "/" + state);
         runOnUiThread(() -> {
             setUiUpdaterState(state);
+
+            if (state == UpdaterState.IDLE) {
+                uiStateIdle();
+            } else if (state == UpdaterState.RUNNING) {
+                uiStateRunning();
+            } else if (state == UpdaterState.PAUSED) {
+                uiStatePaused();
+            } else if (state == UpdaterState.ERROR) {
+                uiStateError();
+            } else if (state == UpdaterState.SLOT_SWITCH_REQUIRED) {
+                uiStateSlotSwitchRequired();
+            } else if (state == UpdaterState.REBOOT_REQUIRED) {
+                uiStateRebootRequired();
+            }
         });
     }
 
@@ -210,17 +248,10 @@
      * be one of the values from {@link UpdateEngine.UpdateStatusConstants}.
      */
     private void onEngineStatusUpdate(int status) {
+        Log.i(TAG, "StatusUpdate - status="
+                + UpdateEngineStatuses.getStatusText(status)
+                + "/" + status);
         runOnUiThread(() -> {
-            Log.e(TAG, "StatusUpdate - status="
-                    + UpdateEngineStatuses.getStatusText(status)
-                    + "/" + status);
-            if (status == UpdateEngine.UpdateStatusConstants.IDLE) {
-                Log.d(TAG, "status changed, resetting ui");
-                uiReset();
-            } else {
-                Log.d(TAG, "status changed, setting ui to updating mode");
-                uiSetUpdating();
-            }
             setUiEngineStatus(status);
         });
     }
@@ -234,19 +265,12 @@
         final String completionState = UpdateEngineErrorCodes.isUpdateSucceeded(errorCode)
                 ? "SUCCESS"
                 : "FAILURE";
+        Log.i(TAG,
+                "PayloadApplicationCompleted - errorCode="
+                        + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
+                        + " " + completionState);
         runOnUiThread(() -> {
-            Log.i(TAG,
-                    "Completed - errorCode="
-                            + UpdateEngineErrorCodes.getCodeName(errorCode) + "/" + errorCode
-                            + " " + completionState);
             setUiEngineErrorCode(errorCode);
-            if (errorCode == UpdateEngineErrorCodes.UPDATED_BUT_NOT_ACTIVE) {
-                // if update was successfully applied.
-                if (mUpdateManager.isManualSwitchSlotRequired()) {
-                    // Show "Switch Slot" button.
-                    uiShowSwitchSlotInfo();
-                }
-            }
         });
     }
 
@@ -258,45 +282,66 @@
     }
 
     /** resets ui */
-    private void uiReset() {
+    private void uiResetWidgets() {
         mTextViewBuild.setText(Build.DISPLAY);
-        mSpinnerConfigs.setEnabled(true);
-        mButtonReload.setEnabled(true);
-        mButtonApplyConfig.setEnabled(true);
+        mSpinnerConfigs.setEnabled(false);
+        mButtonReload.setEnabled(false);
+        mButtonApplyConfig.setEnabled(false);
         mButtonStop.setEnabled(false);
         mButtonReset.setEnabled(false);
-        mProgressBar.setProgress(0);
         mProgressBar.setEnabled(false);
         mProgressBar.setVisibility(ProgressBar.INVISIBLE);
-        uiHideSwitchSlotInfo();
+        mButtonSwitchSlot.setEnabled(false);
+        mTextViewUpdateInfo.setTextColor(Color.parseColor("#aaaaaa"));
     }
 
     private void uiResetEngineText() {
         mTextViewEngineStatus.setText(R.string.unknown);
         mTextViewEngineErrorCode.setText(R.string.unknown);
-        // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies properly.
+        // Note: Do not reset mTextViewUpdaterState; UpdateManager notifies updater state properly.
     }
 
-    /** sets ui updating mode */
-    private void uiSetUpdating() {
-        mTextViewBuild.setText(Build.DISPLAY);
-        mSpinnerConfigs.setEnabled(false);
-        mButtonReload.setEnabled(false);
-        mButtonApplyConfig.setEnabled(false);
-        mButtonStop.setEnabled(true);
+    private void uiStateIdle() {
+        uiResetWidgets();
+        mSpinnerConfigs.setEnabled(true);
+        mButtonReload.setEnabled(true);
+        mButtonApplyConfig.setEnabled(true);
+        mProgressBar.setProgress(0);
+    }
+
+    private void uiStateRunning() {
+        uiResetWidgets();
         mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+        mButtonStop.setEnabled(true);
+    }
+
+    private void uiStatePaused() {
+        uiResetWidgets();
         mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
         mProgressBar.setVisibility(ProgressBar.VISIBLE);
     }
 
-    private void uiShowSwitchSlotInfo() {
+    private void uiStateSlotSwitchRequired() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
         mButtonSwitchSlot.setEnabled(true);
         mTextViewUpdateInfo.setTextColor(Color.parseColor("#777777"));
     }
 
-    private void uiHideSwitchSlotInfo() {
-        mTextViewUpdateInfo.setTextColor(Color.parseColor("#AAAAAA"));
-        mButtonSwitchSlot.setEnabled(false);
+    private void uiStateError() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
+        mProgressBar.setEnabled(true);
+        mProgressBar.setVisibility(ProgressBar.VISIBLE);
+    }
+
+    private void uiStateRebootRequired() {
+        uiResetWidgets();
+        mButtonReset.setEnabled(true);
     }
 
     /**
diff --git a/updater_sample/tests/res/raw/update_config_stream_001.json b/updater_sample/tests/res/raw/update_config_001_stream.json
similarity index 100%
rename from updater_sample/tests/res/raw/update_config_stream_001.json
rename to updater_sample/tests/res/raw/update_config_001_stream.json
diff --git a/updater_sample/tests/res/raw/update_config_stream_002.json b/updater_sample/tests/res/raw/update_config_002_stream.json
similarity index 100%
rename from updater_sample/tests/res/raw/update_config_stream_002.json
rename to updater_sample/tests/res/raw/update_config_002_stream.json
diff --git a/updater_sample/tests/res/raw/update_config_003_nonstream.json b/updater_sample/tests/res/raw/update_config_003_nonstream.json
new file mode 100644
index 0000000..4175c35
--- /dev/null
+++ b/updater_sample/tests/res/raw/update_config_003_nonstream.json
@@ -0,0 +1,9 @@
+{
+    "__": "*** Generated using tools/gen_update_config.py ***",
+    "ab_config": {
+        "force_switch_slot": false
+    },
+    "ab_install_type": "NON_STREAMING",
+    "name": "S ota_002_package",
+    "url": "file:///data/sample-ota-packages/ota_003_package.zip"
+}
\ No newline at end of file
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 000f566..1cbd860 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateConfigTest.java
@@ -60,7 +60,7 @@
     public void setUp() throws Exception {
         mContext = InstrumentationRegistry.getContext();
         mTargetContext = InstrumentationRegistry.getTargetContext();
-        mJsonStreaming001 = readResource(R.raw.update_config_stream_001);
+        mJsonStreaming001 = readResource(R.raw.update_config_001_stream);
     }
 
     @Test
diff --git a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
index 0657a5e..e05ad29 100644
--- a/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
+++ b/updater_sample/tests/src/com/example/android/systemupdatersample/UpdateManagerTest.java
@@ -18,19 +18,22 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import android.content.Context;
 import android.os.UpdateEngine;
 import android.os.UpdateEngineCallback;
+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.example.android.systemupdatersample.util.PayloadSpecs;
+import com.google.common.collect.ImmutableList;
+import com.google.common.io.CharStreams;
 
 import org.junit.Before;
 import org.junit.Rule;
@@ -40,7 +43,9 @@
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
 
-import java.util.function.IntConsumer;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
 
 /**
  * Tests for {@link UpdateManager}
@@ -56,37 +61,86 @@
     private UpdateEngine mUpdateEngine;
     @Mock
     private PayloadSpecs mPayloadSpecs;
-    private UpdateManager mUpdateManager;
+    private UpdateManager mSubject;
+    private Context mContext;
+    private UpdateConfig mNonStreamingUpdate003;
 
     @Before
-    public void setUp() {
-        mUpdateManager = new UpdateManager(mUpdateEngine, mPayloadSpecs);
+    public void setUp() throws Exception {
+        mContext = InstrumentationRegistry.getContext();
+        mSubject = new UpdateManager(mUpdateEngine, mPayloadSpecs);
+        mNonStreamingUpdate003 =
+                UpdateConfig.fromJson(readResource(R.raw.update_config_003_nonstream));
     }
 
     @Test
-    public void storesProgressThenInvokesCallbacks() {
-        IntConsumer statusUpdateCallback = mock(IntConsumer.class);
-
-        // When UpdateManager is bound to update_engine, it passes
-        // UpdateManager.UpdateEngineCallbackImpl as a callback to update_engine.
+    public void applyUpdate_appliesPayloadToUpdateEngine() throws Exception {
+        PayloadSpec payload = buildMockPayloadSpec();
+        when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
         when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
+            // When UpdateManager is bound to update_engine, it passes
+            // UpdateEngineCallback as a callback to update_engine.
             UpdateEngineCallback callback = answer.getArgument(0);
-            callback.onStatusUpdate(/*engineStatus*/ 4, /*engineProgress*/ 0.2f);
+            callback.onStatusUpdate(
+                    UpdateEngine.UpdateStatusConstants.IDLE,
+                    /*engineProgress*/ 0.0f);
             return null;
         });
 
-        mUpdateManager.setOnEngineStatusUpdateCallback(statusUpdateCallback);
+        mSubject.bind();
+        mSubject.applyUpdate(null, mNonStreamingUpdate003);
 
-        // Making sure that manager.getProgress() returns correct progress
-        // in "onEngineStatusUpdate" callback.
-        doAnswer(answer -> {
-            assertEquals(0.2f, mUpdateManager.getProgress(), 1E-5);
+        verify(mUpdateEngine).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[] {
+                        "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
+                });
+    }
+
+    @Test
+    public void stateIsRunningAndEngineStatusIsIdle_reApplyLastUpdate() throws Exception {
+        PayloadSpec payload = buildMockPayloadSpec();
+        when(mPayloadSpecs.forNonStreaming(any(File.class))).thenReturn(payload);
+        when(mUpdateEngine.bind(any(UpdateEngineCallback.class))).thenAnswer(answer -> {
+            // When UpdateManager is bound to update_engine, it passes
+            // UpdateEngineCallback as a callback to update_engine.
+            UpdateEngineCallback callback = answer.getArgument(0);
+            callback.onStatusUpdate(
+                    UpdateEngine.UpdateStatusConstants.IDLE,
+                    /*engineProgress*/ 0.0f);
             return null;
-        }).when(statusUpdateCallback).accept(anyInt());
+        });
 
-        mUpdateManager.bind();
+        mSubject.bind();
+        mSubject.applyUpdate(null, mNonStreamingUpdate003);
+        mSubject.unbind();
+        mSubject.bind(); // re-bind - now it should re-apply last update
 
-        verify(statusUpdateCallback, times(1)).accept(4);
+        assertEquals(mSubject.getUpdaterState(), UpdaterState.RUNNING);
+        // it should be called 2 times
+        verify(mUpdateEngine, times(2)).applyPayload(
+                "file://blah",
+                120,
+                340,
+                new String[] {
+                        "SWITCH_SLOT_ON_REBOOT=0" // ab_config.force_switch_slot = false
+                });
+    }
+
+    private PayloadSpec buildMockPayloadSpec() {
+        PayloadSpec payload = mock(PayloadSpec.class);
+        when(payload.getUrl()).thenReturn("file://blah");
+        when(payload.getOffset()).thenReturn(120L);
+        when(payload.getSize()).thenReturn(340L);
+        when(payload.getProperties()).thenReturn(ImmutableList.of());
+        return payload;
+    }
+
+    private String readResource(int id) throws IOException {
+        return CharStreams.toString(new InputStreamReader(
+                mContext.getResources().openRawResource(id)));
     }
 
 }