Add support for new payload property headers to delay switching slots.

Set SWITCH_SLOT_ON_REBOOT=0 to skip marking new slot as active.
When ready to reboot to the new update, call applyPayload() again to
switch the slots, download will be skipped because it has already
finished, filesystem verification will always happen, and postinstall
can be skipped if it succeeded before for this update and
RUN_POST_INSTALL is set to 0.

Also removed reset update progress when update succeeded to support
reverting to current slot with resetStatus() without clearing download
progress. If the next update is a different payload then we will still
reset the progress on next update.

Bug: 35212183
Test: update_device.py --extra-headers 'SWITCH_SLOT_ON_REBOOT=0' ...

Merged-In: I52e2371ea4a9e6a6d026b4dd04bb1a60d95c9d5c
Change-Id: I52e2371ea4a9e6a6d026b4dd04bb1a60d95c9d5c
diff --git a/common/constants.cc b/common/constants.cc
index c0a6e27..5941c93 100644
--- a/common/constants.cc
+++ b/common/constants.cc
@@ -57,6 +57,7 @@
 const char kPrefsP2PFirstAttemptTimestamp[] = "p2p-first-attempt-timestamp";
 const char kPrefsP2PNumAttempts[] = "p2p-num-attempts";
 const char kPrefsPayloadAttemptNumber[] = "payload-attempt-number";
+const char kPrefsPostInstallSucceeded[] = "post-install-succeeded";
 const char kPrefsPreviousVersion[] = "previous-version";
 const char kPrefsResumedUpdateFailures[] = "resumed-update-failures";
 const char kPrefsRollbackVersion[] = "rollback-version";
@@ -103,5 +104,12 @@
 // This can be used to zero-rate OTA traffic by sending it over the correct
 // network.
 const char kPayloadPropertyNetworkId[] = "NETWORK_ID";
+// Set "SWITCH_SLOT_ON_REBOOT=0" to skip marking the updated partitions active.
+// The default is 1 (always switch slot if update succeeded).
+const char kPayloadPropertySwitchSlotOnReboot[] = "SWITCH_SLOT_ON_REBOOT";
+// Set "RUN_POST_INSTALL=0" to skip running post install, this will only be
+// honored if we're resuming an update and post install has already succeeded.
+// The default is 1 (always run post install).
+const char kPayloadPropertyRunPostInstall[] = "RUN_POST_INSTALL";
 
 }  // namespace chromeos_update_engine
diff --git a/common/constants.h b/common/constants.h
index 776e726..fefd08c 100644
--- a/common/constants.h
+++ b/common/constants.h
@@ -59,6 +59,7 @@
 extern const char kPrefsP2PFirstAttemptTimestamp[];
 extern const char kPrefsP2PNumAttempts[];
 extern const char kPrefsPayloadAttemptNumber[];
+extern const char kPrefsPostInstallSucceeded[];
 extern const char kPrefsPreviousVersion[];
 extern const char kPrefsResumedUpdateFailures[];
 extern const char kPrefsRollbackVersion[];
@@ -96,6 +97,8 @@
 extern const char kPayloadPropertyUserAgent[];
 extern const char kPayloadPropertyPowerwash[];
 extern const char kPayloadPropertyNetworkId[];
+extern const char kPayloadPropertySwitchSlotOnReboot[];
+extern const char kPayloadPropertyRunPostInstall[];
 
 // A download source is any combination of protocol and server (that's of
 // interest to us when looking at UMA metrics) using which we may download
diff --git a/common/error_code.h b/common/error_code.h
index e08ec46..adae391 100644
--- a/common/error_code.h
+++ b/common/error_code.h
@@ -73,6 +73,9 @@
   kFilesystemVerifierError = 47,
   kUserCanceled = 48,
   kNonCriticalUpdateInOOBE = 49,
+  // kOmahaUpdateIgnoredOverCellular = 50,
+  // kPayloadTimestampError = 51,
+  kUpdatedButNotActive = 52,
 
   // VERY IMPORTANT! When adding new error codes:
   //
diff --git a/common/error_code_utils.cc b/common/error_code_utils.cc
index ad4aeeb..983d004 100644
--- a/common/error_code_utils.cc
+++ b/common/error_code_utils.cc
@@ -144,8 +144,10 @@
       return "ErrorCode::kUserCanceled";
     case ErrorCode::kNonCriticalUpdateInOOBE:
       return "ErrorCode::kNonCriticalUpdateInOOBE";
-    // Don't add a default case to let the compiler warn about newly added
-    // error codes which should be added here.
+    case ErrorCode::kUpdatedButNotActive:
+      return "ErrorCode::kUpdatedButNotActive";
+      // Don't add a default case to let the compiler warn about newly added
+      // error codes which should be added here.
   }
 
   return "Unknown error: " + base::UintToString(static_cast<unsigned>(code));
diff --git a/metrics_constants.h b/metrics_constants.h
index f6d0c74..abec2ad 100644
--- a/metrics_constants.h
+++ b/metrics_constants.h
@@ -95,6 +95,8 @@
   kPostInstallFailed,           // The postinstall step failed.
   kAbnormalTermination,         // The attempt ended abnormally.
   kUpdateCanceled,              // Update canceled by the user.
+  kUpdateSucceededNotActive,    // Update succeeded but the new slot is not
+                                // active.
 
   kNumConstants,
 
diff --git a/metrics_utils.cc b/metrics_utils.cc
index 5cff293..7e6b20f 100644
--- a/metrics_utils.cc
+++ b/metrics_utils.cc
@@ -39,6 +39,9 @@
     case ErrorCode::kSuccess:
       return metrics::AttemptResult::kUpdateSucceeded;
 
+    case ErrorCode::kUpdatedButNotActive:
+      return metrics::AttemptResult::kUpdateSucceededNotActive;
+
     case ErrorCode::kDownloadTransferError:
       return metrics::AttemptResult::kPayloadDownloadError;
 
@@ -210,6 +213,7 @@
     case ErrorCode::kOmahaRequestXMLHasEntityDecl:
     case ErrorCode::kFilesystemVerifierError:
     case ErrorCode::kUserCanceled:
+    case ErrorCode::kUpdatedButNotActive:
       break;
 
     // Special flags. These can't happen (we mask them out above) but
diff --git a/payload_consumer/delta_performer.cc b/payload_consumer/delta_performer.cc
index c1afa44..d384e45 100644
--- a/payload_consumer/delta_performer.cc
+++ b/payload_consumer/delta_performer.cc
@@ -1630,6 +1630,7 @@
     prefs->SetInt64(kPrefsManifestMetadataSize, -1);
     prefs->SetInt64(kPrefsManifestSignatureSize, -1);
     prefs->SetInt64(kPrefsResumedUpdateFailures, 0);
+    prefs->Delete(kPrefsPostInstallSucceeded);
   }
   return true;
 }
diff --git a/payload_consumer/install_plan.cc b/payload_consumer/install_plan.cc
index b0dff31..45112d6 100644
--- a/payload_consumer/install_plan.cc
+++ b/payload_consumer/install_plan.cc
@@ -87,7 +87,10 @@
             << ", url: " << download_url << payloads_str << partitions_str
             << ", hash_checks_mandatory: "
             << utils::ToString(hash_checks_mandatory)
-            << ", powerwash_required: " << utils::ToString(powerwash_required);
+            << ", powerwash_required: " << utils::ToString(powerwash_required)
+            << ", switch_slot_on_reboot: "
+            << utils::ToString(switch_slot_on_reboot)
+            << ", run_post_install: " << utils::ToString(run_post_install);
 }
 
 bool InstallPlan::LoadPartitionsFromSlots(BootControlInterface* boot_control) {
diff --git a/payload_consumer/install_plan.h b/payload_consumer/install_plan.h
index 551f8c2..5cdfbc1 100644
--- a/payload_consumer/install_plan.h
+++ b/payload_consumer/install_plan.h
@@ -119,6 +119,14 @@
   // False otherwise.
   bool powerwash_required{false};
 
+  // True if the updated slot should be marked active on success.
+  // False otherwise.
+  bool switch_slot_on_reboot{true};
+
+  // True if the update should run its post-install step.
+  // False otherwise.
+  bool run_post_install{true};
+
   // If not blank, a base-64 encoded representation of the PEM-encoded
   // public key in the response.
   std::string public_key_rsa;
diff --git a/payload_consumer/postinstall_runner_action.cc b/payload_consumer/postinstall_runner_action.cc
index 27a9ed6..cedecda 100644
--- a/payload_consumer/postinstall_runner_action.cc
+++ b/payload_consumer/postinstall_runner_action.cc
@@ -82,6 +82,11 @@
 }
 
 void PostinstallRunnerAction::PerformPartitionPostinstall() {
+  if (!install_plan_.run_post_install) {
+    LOG(INFO) << "Skipping post-install according to install plan.";
+    return CompletePostinstall(ErrorCode::kSuccess);
+  }
+
   if (install_plan_.download_url.empty()) {
     LOG(INFO) << "Skipping post-install during rollback";
     return CompletePostinstall(ErrorCode::kSuccess);
@@ -331,15 +336,21 @@
 void PostinstallRunnerAction::CompletePostinstall(ErrorCode error_code) {
   // We only attempt to mark the new slot as active if all the postinstall
   // steps succeeded.
-  if (error_code == ErrorCode::kSuccess &&
-      !boot_control_->SetActiveBootSlot(install_plan_.target_slot)) {
-    error_code = ErrorCode::kPostinstallRunnerError;
+  if (error_code == ErrorCode::kSuccess) {
+    if (install_plan_.switch_slot_on_reboot) {
+      if (!boot_control_->SetActiveBootSlot(install_plan_.target_slot)) {
+        error_code = ErrorCode::kPostinstallRunnerError;
+      }
+    } else {
+      error_code = ErrorCode::kUpdatedButNotActive;
+    }
   }
 
   ScopedActionCompleter completer(processor_, this);
   completer.set_code(error_code);
 
-  if (error_code != ErrorCode::kSuccess) {
+  if (error_code != ErrorCode::kSuccess &&
+      error_code != ErrorCode::kUpdatedButNotActive) {
     LOG(ERROR) << "Postinstall action failed.";
 
     // Undo any changes done to trigger Powerwash.
diff --git a/payload_state.cc b/payload_state.cc
index 1ec32c5..cff02b1 100644
--- a/payload_state.cc
+++ b/payload_state.cc
@@ -357,6 +357,7 @@
     case ErrorCode::kOmahaRequestXMLHasEntityDecl:
     case ErrorCode::kFilesystemVerifierError:
     case ErrorCode::kUserCanceled:
+    case ErrorCode::kUpdatedButNotActive:
       LOG(INFO) << "Not incrementing URL index or failure count for this error";
       break;
 
@@ -633,6 +634,7 @@
     case metrics::AttemptResult::kPostInstallFailed:
     case metrics::AttemptResult::kAbnormalTermination:
     case metrics::AttemptResult::kUpdateCanceled:
+    case metrics::AttemptResult::kUpdateSucceededNotActive:
     case metrics::AttemptResult::kNumConstants:
     case metrics::AttemptResult::kUnset:
       break;
diff --git a/update_attempter_android.cc b/update_attempter_android.cc
index 7dcbeec..e716296 100644
--- a/update_attempter_android.cc
+++ b/update_attempter_android.cc
@@ -80,6 +80,13 @@
   return false;
 }
 
+bool GetHeaderAsBool(const string& header, bool default_value) {
+  int value = 0;
+  if (base::StringToInt(header, &value) && (value == 0 || value == 1))
+    return value == 1;
+  return default_value;
+}
+
 }  // namespace
 
 UpdateAttempterAndroid::UpdateAttempterAndroid(
@@ -192,10 +199,25 @@
   install_plan_.source_slot = boot_control_->GetCurrentSlot();
   install_plan_.target_slot = install_plan_.source_slot == 0 ? 1 : 0;
 
-  int data_wipe = 0;
   install_plan_.powerwash_required =
-      base::StringToInt(headers[kPayloadPropertyPowerwash], &data_wipe) &&
-      data_wipe != 0;
+      GetHeaderAsBool(headers[kPayloadPropertyPowerwash], false);
+
+  install_plan_.switch_slot_on_reboot =
+      GetHeaderAsBool(headers[kPayloadPropertySwitchSlotOnReboot], true);
+
+  install_plan_.run_post_install = true;
+  // Optionally skip post install if and only if:
+  // a) we're resuming
+  // b) post install has already succeeded before
+  // c) RUN_POST_INSTALL is set to 0.
+  if (install_plan_.is_resume && prefs_->Exists(kPrefsPostInstallSucceeded)) {
+    bool post_install_succeeded = false;
+    prefs_->GetBoolean(kPrefsPostInstallSucceeded, &post_install_succeeded);
+    if (post_install_succeeded) {
+      install_plan_.run_post_install =
+          GetHeaderAsBool(headers[kPayloadPropertyRunPostInstall], true);
+    }
+  }
 
   NetworkId network_id = kDefaultNetworkId;
   if (!headers[kPayloadPropertyNetworkId].empty()) {
@@ -313,7 +335,6 @@
       // Update succeeded.
       WriteUpdateCompletedMarker();
       prefs_->SetInt64(kPrefsDeltaUpdateFailures, 0);
-      DeltaPerformer::ResetUpdateProgress(prefs_, false);
 
       LOG(INFO) << "Update successfully applied, waiting to reboot.";
       break;
@@ -351,6 +372,11 @@
   if (type == DownloadAction::StaticType()) {
     download_progress_ = 0;
   }
+  if (type == PostinstallRunnerAction::StaticType()) {
+    bool succeeded =
+        code == ErrorCode::kSuccess || code == ErrorCode::kUpdatedButNotActive;
+    prefs_->SetBoolean(kPrefsPostInstallSucceeded, succeeded);
+  }
   if (code != ErrorCode::kSuccess) {
     // If an action failed, the ActionProcessor will cancel the whole thing.
     return;
diff --git a/update_engine_client_android.cc b/update_engine_client_android.cc
index 989a97e..267f6e9 100644
--- a/update_engine_client_android.cc
+++ b/update_engine_client_android.cc
@@ -97,7 +97,10 @@
   ErrorCode code = static_cast<ErrorCode>(error_code);
   LOG(INFO) << "onPayloadApplicationComplete(" << utils::ErrorCodeToString(code)
             << " (" << error_code << "))";
-  client_->ExitWhenIdle(code == ErrorCode::kSuccess ? EX_OK : 1);
+  client_->ExitWhenIdle(
+      (code == ErrorCode::kSuccess || code == ErrorCode::kUpdatedButNotActive)
+          ? EX_OK
+          : 1);
   return Status::ok();
 }
 
diff --git a/update_manager/chromeos_policy.cc b/update_manager/chromeos_policy.cc
index ffd378c..44f8821 100644
--- a/update_manager/chromeos_policy.cc
+++ b/update_manager/chromeos_policy.cc
@@ -134,6 +134,7 @@
     case ErrorCode::kOmahaRequestXMLHasEntityDecl:
     case ErrorCode::kFilesystemVerifierError:
     case ErrorCode::kUserCanceled:
+    case ErrorCode::kUpdatedButNotActive:
       LOG(INFO) << "Not changing URL index or failure count due to error "
                 << chromeos_update_engine::utils::ErrorCodeToString(err_code)
                 << " (" << static_cast<int>(err_code) << ")";