power: Suspend instead of shutting down for TPM counter.

Add a tpm_counter_suspend_threshold powerd pref. If the
TPM's dictionary-attack counter reaches this value, idle and
lid-closed "shut down" actions will be overridden to
suspend instead.

Also give the "power" user permission to call cryptohomed's
GetTpmStatus D-Bus methoed.

BUG=chromium:462428
TEST=added unit tests; also manually tested by setting the
     pref and watching /var/log/power_manager/powerd.LATEST
CQ-DEPEND=CL:254570

Change-Id: I3c5944ea5faaf78eedc074a1292f4d8124194d45
Previous-Reviewed-on: https://chromium-review.googlesource.com/254670
(cherry picked from commit 696bd299934f40c706f2ac378a6995336efbbf90)
Reviewed-on: https://chromium-review.googlesource.com/259280
Reviewed-by: Dan Erat <derat@chromium.org>
Reviewed-by: Luigi Semenzato <semenzato@chromium.org>
Commit-Queue: Luigi Semenzato <semenzato@chromium.org>
Tested-by: Luigi Semenzato <semenzato@chromium.org>
diff --git a/cryptohome/cryptohome.cc b/cryptohome/cryptohome.cc
index c983c8e..c7a7151 100644
--- a/cryptohome/cryptohome.cc
+++ b/cryptohome/cryptohome.cc
@@ -1571,7 +1571,7 @@
                      action.c_str())) {
     cryptohome::GetTpmStatusRequest request;
     cryptohome::BaseReply reply;
-    if (!MakeProtoDBusCall("GetTpmStatus",
+    if (!MakeProtoDBusCall(cryptohome::kCryptohomeGetTpmStatus,
                            DBUS_METHOD(get_tpm_status),
                            DBUS_METHOD(get_tpm_status_async),
                            cl, &proxy, request, &reply)) {
diff --git a/cryptohome/etc/Cryptohome.conf b/cryptohome/etc/Cryptohome.conf
index ddb848a..ba57f0e 100644
--- a/cryptohome/etc/Cryptohome.conf
+++ b/cryptohome/etc/Cryptohome.conf
@@ -258,6 +258,11 @@
            send_interface="org.chromium.CryptohomeInterface"
            send_member="FlushAndSignBootAttributes"/>
   </policy>
+  <policy user="power">
+    <allow send_destination="org.chromium.Cryptohome"
+           send_interface="org.chromium.CryptohomeInterface"
+           send_member="GetTpmStatus"/>
+  </policy>
   <policy context="default">
     <deny send_destination="org.chromium.Cryptohome" />
   </policy>
diff --git a/power_manager/common/power_constants.cc b/power_manager/common/power_constants.cc
index 5ddbd18..928438a 100644
--- a/power_manager/common/power_constants.cc
+++ b/power_manager/common/power_constants.cc
@@ -72,6 +72,7 @@
 const char kMosysEventlogPref[] = "mosys_eventlog";
 const char kCheckActiveVTPref[] = "check_active_vt";
 const char kUseCrasPref[] = "use_cras";
+const char kTpmCounterSuspendThresholdPref[] = "tpm_counter_suspend_threshold";
 
 // Miscellaneous constants.
 const char kReadWritePrefsDir[] = "/var/lib/power_manager";
diff --git a/power_manager/common/power_constants.h b/power_manager/common/power_constants.h
index fde20a9..17f8d5d 100644
--- a/power_manager/common/power_constants.h
+++ b/power_manager/common/power_constants.h
@@ -173,6 +173,11 @@
 // to mute audio when suspending.
 extern const char kUseCrasPref[];
 
+// Integer TPM dictionary-attack counter value at or above which the system will
+// suspend instead of shutting down in response to idle or lid-close (see
+// http://crbug.com/462428). Set to 0 to disable querying the TPM.
+extern const char kTpmCounterSuspendThresholdPref[];
+
 // Miscellaneous constants.
 
 // Default directories where read/write and read-only powerd preference files
diff --git a/power_manager/default_prefs/tpm_counter_suspend_threshold b/power_manager/default_prefs/tpm_counter_suspend_threshold
new file mode 100644
index 0000000..573541a
--- /dev/null
+++ b/power_manager/default_prefs/tpm_counter_suspend_threshold
@@ -0,0 +1 @@
+0
diff --git a/power_manager/powerd/daemon.cc b/power_manager/powerd/daemon.cc
index 2526b11..7d14f49 100644
--- a/power_manager/powerd/daemon.cc
+++ b/power_manager/powerd/daemon.cc
@@ -17,6 +17,7 @@
 #include <chromeos/dbus/service_constants.h>
 #include <metrics/metrics_library.h>
 
+#include "cryptohome/proto_bindings/rpc.pb.h"
 #include "power_manager/common/dbus_sender.h"
 #include "power_manager/common/metrics_sender.h"
 #include "power_manager/common/power_constants.h"
@@ -83,6 +84,7 @@
 // processes.
 const int kSessionManagerDBusTimeoutMs = 3000;
 const int kUpdateEngineDBusTimeoutMs = 3000;
+const int kCryptohomedDBusTimeoutMs = 2 * 60 * 1000;  // Two minutes.
 
 // If we go from a dark resume directly to a full resume several devices will be
 // left in an awkward state.  This won't be a problem once selective resume is
@@ -320,6 +322,7 @@
       session_manager_dbus_proxy_(NULL),
       cras_dbus_proxy_(NULL),
       update_engine_dbus_proxy_(NULL),
+      cryptohomed_dbus_proxy_(NULL),
       state_controller_delegate_(new StateControllerDelegate(this)),
       dbus_sender_(new DBusSender),
       display_watcher_(new system::DisplayWatcher),
@@ -344,7 +347,8 @@
       lock_vt_before_suspend_(false),
       log_suspend_with_mosys_eventlog_(false),
       can_safely_exit_dark_resume_(
-          base::PathExists(base::FilePath(kPMTestDelayPath))) {
+          base::PathExists(base::FilePath(kPMTestDelayPath))),
+      weak_ptr_factory_(this) {
   scoped_ptr<MetricsLibrary> metrics_lib(new MetricsLibrary);
   metrics_lib->Init();
   metrics_sender_.reset(new MetricsSender(metrics_lib.Pass()));
@@ -789,20 +793,21 @@
       dbus::ObjectPath(chromeos::kLibCrosServicePath));
   chrome_dbus_proxy_->WaitForServiceToBeAvailable(
       base::Bind(&Daemon::HandleChromeAvailableOrRestarted,
-                 base::Unretained(this)));
+                 weak_ptr_factory_.GetWeakPtr()));
 
   session_manager_dbus_proxy_ = bus_->GetObjectProxy(
       login_manager::kSessionManagerServiceName,
       dbus::ObjectPath(login_manager::kSessionManagerServicePath));
   session_manager_dbus_proxy_->WaitForServiceToBeAvailable(
       base::Bind(&Daemon::HandleSessionManagerAvailableOrRestarted,
-                 base::Unretained(this)));
+                 weak_ptr_factory_.GetWeakPtr()));
   session_manager_dbus_proxy_->ConnectToSignal(
       login_manager::kSessionManagerInterface,
       login_manager::kSessionStateChangedSignal,
       base::Bind(&Daemon::HandleSessionStateChangedSignal,
-                 base::Unretained(this)),
-      base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+                 weak_ptr_factory_.GetWeakPtr()),
+      base::Bind(&Daemon::HandleDBusSignalConnected,
+                 weak_ptr_factory_.GetWeakPtr()));
 
   if (audio_client_) {
     cras_dbus_proxy_ = bus_->GetObjectProxy(
@@ -810,39 +815,54 @@
         dbus::ObjectPath(cras::kCrasServicePath));
     cras_dbus_proxy_->WaitForServiceToBeAvailable(
         base::Bind(&Daemon::HandleCrasAvailableOrRestarted,
-                   base::Unretained(this)));
+                   weak_ptr_factory_.GetWeakPtr()));
     cras_dbus_proxy_->ConnectToSignal(
         cras::kCrasControlInterface,
         cras::kNodesChanged,
         base::Bind(&Daemon::HandleCrasNodesChangedSignal,
-                   base::Unretained(this)),
-        base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+                   weak_ptr_factory_.GetWeakPtr()),
+        base::Bind(&Daemon::HandleDBusSignalConnected,
+                   weak_ptr_factory_.GetWeakPtr()));
     cras_dbus_proxy_->ConnectToSignal(
         cras::kCrasControlInterface,
         cras::kActiveOutputNodeChanged,
         base::Bind(&Daemon::HandleCrasActiveOutputNodeChangedSignal,
-                   base::Unretained(this)),
-        base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+                   weak_ptr_factory_.GetWeakPtr()),
+        base::Bind(&Daemon::HandleDBusSignalConnected,
+                   weak_ptr_factory_.GetWeakPtr()));
     cras_dbus_proxy_->ConnectToSignal(
         cras::kCrasControlInterface,
         cras::kNumberOfActiveStreamsChanged,
         base::Bind(&Daemon::HandleCrasNumberOfActiveStreamsChanged,
-                   base::Unretained(this)),
-        base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+                   weak_ptr_factory_.GetWeakPtr()),
+        base::Bind(&Daemon::HandleDBusSignalConnected,
+                   weak_ptr_factory_.GetWeakPtr()));
   }
 
   update_engine_dbus_proxy_ = bus_->GetObjectProxy(
       update_engine::kUpdateEngineServiceName,
       dbus::ObjectPath(update_engine::kUpdateEngineServicePath));
   update_engine_dbus_proxy_->WaitForServiceToBeAvailable(
-      base::Bind(&Daemon::HandleUpdateEngineAvailableOrRestarted,
-                 base::Unretained(this)));
+      base::Bind(&Daemon::HandleUpdateEngineAvailable,
+                 weak_ptr_factory_.GetWeakPtr()));
   update_engine_dbus_proxy_->ConnectToSignal(
       update_engine::kUpdateEngineInterface,
       update_engine::kStatusUpdate,
       base::Bind(&Daemon::HandleUpdateEngineStatusUpdateSignal,
-                 base::Unretained(this)),
-      base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+                 weak_ptr_factory_.GetWeakPtr()),
+      base::Bind(&Daemon::HandleDBusSignalConnected,
+                 weak_ptr_factory_.GetWeakPtr()));
+
+  int64 tpm_threshold = 0;
+  prefs_->GetInt64(kTpmCounterSuspendThresholdPref, &tpm_threshold);
+  if (tpm_threshold > 0) {
+    cryptohomed_dbus_proxy_ = bus_->GetObjectProxy(
+        cryptohome::kCryptohomeServiceName,
+        dbus::ObjectPath(cryptohome::kCryptohomeServicePath));
+    cryptohomed_dbus_proxy_->WaitForServiceToBeAvailable(
+        base::Bind(&Daemon::HandleCryptohomedAvailable,
+                   weak_ptr_factory_.GetWeakPtr()));
+  }
 
   powerd_dbus_object_ = bus_->GetExportedObject(
       dbus::ObjectPath(kPowerManagerServicePath));
@@ -921,8 +941,10 @@
   dbus::ObjectProxy* proxy = bus_->GetObjectProxy(
       kBusServiceName, dbus::ObjectPath(kBusServicePath));
   proxy->ConnectToSignal(kBusInterface, kNameOwnerChangedSignal,
-      base::Bind(&Daemon::HandleDBusNameOwnerChanged, base::Unretained(this)),
-      base::Bind(&Daemon::HandleDBusSignalConnected, base::Unretained(this)));
+      base::Bind(&Daemon::HandleDBusNameOwnerChanged,
+                 weak_ptr_factory_.GetWeakPtr()),
+      base::Bind(&Daemon::HandleDBusSignalConnected,
+                 weak_ptr_factory_.GetWeakPtr()));
 
   dbus_sender_->Init(powerd_dbus_object_, kPowerManagerInterface);
 }
@@ -970,7 +992,7 @@
     audio_client_->LoadInitialState();
 }
 
-void Daemon::HandleUpdateEngineAvailableOrRestarted(bool available) {
+void Daemon::HandleUpdateEngineAvailable(bool available) {
   if (!available) {
     LOG(ERROR) << "Failed waiting for update engine to become available";
     return;
@@ -997,6 +1019,23 @@
   OnUpdateOperation(operation);
 }
 
+void Daemon::HandleCryptohomedAvailable(bool available) {
+  if (!available) {
+    LOG(ERROR) << "Failed waiting for cryptohomed to become available";
+    return;
+  }
+  if (cryptohomed_dbus_proxy_) {
+    dbus::MethodCall method_call(cryptohome::kCryptohomeInterface,
+                                 cryptohome::kCryptohomeGetTpmStatus);
+    dbus::MessageWriter writer(&method_call);
+    writer.AppendProtoAsArrayOfBytes(cryptohome::GetTpmStatusRequest());
+    cryptohomed_dbus_proxy_->CallMethod(
+        &method_call, kCryptohomedDBusTimeoutMs,
+        base::Bind(&Daemon::HandleGetTpmStatusResponse,
+                   weak_ptr_factory_.GetWeakPtr()));
+  }
+}
+
 void Daemon::HandleDBusNameOwnerChanged(dbus::Signal* signal) {
   dbus::MessageReader reader(signal);
   std::string name, old_owner, new_owner;
@@ -1076,6 +1115,38 @@
   audio_client_->UpdateNumActiveStreams();
 }
 
+void Daemon::HandleGetTpmStatusResponse(dbus::Response* response) {
+  if (!response) {
+    LOG(ERROR) << cryptohome::kCryptohomeGetTpmStatus << " call failed";
+    return;
+  }
+
+  cryptohome::BaseReply base_reply;
+  dbus::MessageReader reader(response);
+  if (!reader.PopArrayOfBytesAsProto(&base_reply)) {
+    LOG(ERROR) << "Unable to parse " << cryptohome::kCryptohomeGetTpmStatus
+               << "response";
+    return;
+  }
+  if (base_reply.has_error()) {
+    LOG(ERROR) << cryptohome::kCryptohomeGetTpmStatus << " response contains "
+               << "error code " << base_reply.error();
+    return;
+  }
+  if (!base_reply.HasExtension(cryptohome::GetTpmStatusReply::reply)) {
+    LOG(ERROR) << cryptohome::kCryptohomeGetTpmStatus << " response doesn't "
+               << "contain nested reply";
+    return;
+  }
+
+  cryptohome::GetTpmStatusReply tpm_reply =
+      base_reply.GetExtension(cryptohome::GetTpmStatusReply::reply);
+  LOG(INFO) << "Received " << cryptohome::kCryptohomeGetTpmStatus
+            << " response with dictionary attack count "
+            << tpm_reply.dictionary_attack_counter();
+  state_controller_->HandleTpmStatus(tpm_reply.dictionary_attack_counter());
+}
+
 scoped_ptr<dbus::Response> Daemon::HandleRequestShutdownMethod(
     dbus::MethodCall* method_call) {
   LOG(INFO) << "Got " << kRequestShutdownMethod << " message from "
@@ -1372,7 +1443,8 @@
     if (!retry_shutdown_for_flashrom_timer_.IsRunning()) {
       retry_shutdown_for_flashrom_timer_.Start(
           FROM_HERE, base::TimeDelta::FromSeconds(kRetryShutdownForFlashromSec),
-          base::Bind(&Daemon::ShutDown, base::Unretained(this), mode, reason));
+          base::Bind(&Daemon::ShutDown, weak_ptr_factory_.GetWeakPtr(), mode,
+                     reason));
     }
     return;
   }
diff --git a/power_manager/powerd/daemon.h b/power_manager/powerd/daemon.h
index 8363b11..29429c8 100644
--- a/power_manager/powerd/daemon.h
+++ b/power_manager/powerd/daemon.h
@@ -14,6 +14,7 @@
 #include <base/files/file_path.h>
 #include <base/macros.h>
 #include <base/memory/scoped_ptr.h>
+#include <base/memory/weak_ptr.h>
 #include <base/time/time.h>
 #include <base/timer/timer.h>
 #include <dbus/bus.h>
@@ -156,7 +157,11 @@
   void HandleChromeAvailableOrRestarted(bool available);
   void HandleSessionManagerAvailableOrRestarted(bool available);
   void HandleCrasAvailableOrRestarted(bool available);
-  void HandleUpdateEngineAvailableOrRestarted(bool available);
+
+  // Handles other D-Bus services just becoming initially available (i.e.
+  // restarts are ignored).
+  void HandleUpdateEngineAvailable(bool available);
+  void HandleCryptohomedAvailable(bool available);
 
   // Handles changes to D-Bus name ownership.
   void HandleDBusNameOwnerChanged(dbus::Signal* signal);
@@ -177,6 +182,7 @@
   void HandleCrasNodesChangedSignal(dbus::Signal* signal);
   void HandleCrasActiveOutputNodeChangedSignal(dbus::Signal* signal);
   void HandleCrasNumberOfActiveStreamsChanged(dbus::Signal* signal);
+  void HandleGetTpmStatusResponse(dbus::Response* response);
   scoped_ptr<dbus::Response> HandleRequestShutdownMethod(
       dbus::MethodCall* method_call);
   scoped_ptr<dbus::Response> HandleRequestRestartMethod(
@@ -239,6 +245,8 @@
   dbus::ObjectProxy* session_manager_dbus_proxy_;  // weak; owned by |bus_|
   dbus::ObjectProxy* cras_dbus_proxy_;  // weak; owned by |bus_| and may be NULL
   dbus::ObjectProxy* update_engine_dbus_proxy_;  // weak; owned by |bus_|
+  // May be null if the TPM status is not needed.
+  dbus::ObjectProxy* cryptohomed_dbus_proxy_;  // weak; owned by |bus_|
 
   scoped_ptr<StateControllerDelegate> state_controller_delegate_;
   scoped_ptr<MetricsSender> metrics_sender_;
@@ -300,6 +308,10 @@
   // resumed.
   bool can_safely_exit_dark_resume_;
 
+  // Must come last so that weak pointers will be invalidated before other
+  // members are destroyed.
+  base::WeakPtrFactory<Daemon> weak_ptr_factory_;
+
   DISALLOW_COPY_AND_ASSIGN(Daemon);
 };
 
diff --git a/power_manager/powerd/policy/state_controller.cc b/power_manager/powerd/policy/state_controller.cc
index 0c9548d..69e39f7 100644
--- a/power_manager/powerd/policy/state_controller.cc
+++ b/power_manager/powerd/policy/state_controller.cc
@@ -236,6 +236,8 @@
       disable_idle_suspend_(false),
       allow_docked_mode_(false),
       ignore_external_policy_(false),
+      tpm_dictionary_attack_count_(0),
+      tpm_dictionary_attack_suspend_threshold_(0),
       audio_is_active_(false),
       idle_action_(DO_NOTHING),
       lid_closed_action_(DO_NOTHING),
@@ -419,6 +421,11 @@
   UpdateState();
 }
 
+void StateController::HandleTpmStatus(int dictionary_attack_count) {
+  tpm_dictionary_attack_count_ = dictionary_attack_count;
+  UpdateSettingsAndState();
+}
+
 void StateController::OnPrefChanged(const std::string& pref_name) {
   CHECK(initialized_);
   if (pref_name == kDisableIdleSuspendPref ||
@@ -608,6 +615,10 @@
   prefs_->GetBool(kIgnoreExternalPolicyPref, &ignore_external_policy_);
   prefs_->GetBool(kAllowDockedModePref, &allow_docked_mode_);
 
+  int64_t tpm_threshold = 0;
+  prefs_->GetInt64(kTpmCounterSuspendThresholdPref, &tpm_threshold);
+  tpm_dictionary_attack_suspend_threshold_ = static_cast<int>(tpm_threshold);
+
   CHECK(GetMillisecondPref(prefs_, kPluggedSuspendMsPref,
                            &pref_ac_delays_.idle));
   CHECK(GetMillisecondPref(prefs_, kPluggedOffMsPref,
@@ -703,6 +714,21 @@
   if (allow_docked_mode_ && presenting)
     lid_closed_action_ = DO_NOTHING;
 
+  // Override the idle and lid-closed actions to suspend instead of shutting
+  // down if the TPM dictionary-attack counter is high.
+  if (tpm_dictionary_attack_suspend_threshold_ > 0 &&
+      tpm_dictionary_attack_count_ >=
+      tpm_dictionary_attack_suspend_threshold_) {
+    LOG(WARNING) << "TPM dictionary attack count is "
+                 << tpm_dictionary_attack_count_ << " (threshold is "
+                 << tpm_dictionary_attack_suspend_threshold_ << "); "
+                 << "overriding actions to suspend instead of shutting down";
+    if (idle_action_ == SHUT_DOWN)
+      idle_action_ = SUSPEND;
+    if (lid_closed_action_ == SHUT_DOWN)
+      lid_closed_action_ = SUSPEND;
+  }
+
   // If the idle or lid-closed actions changed, make sure that we perform
   // the new actions in the event that the system is already idle or the
   // lid is already closed.
diff --git a/power_manager/powerd/policy/state_controller.h b/power_manager/powerd/policy/state_controller.h
index 24d5a18..e756869 100644
--- a/power_manager/powerd/policy/state_controller.h
+++ b/power_manager/powerd/policy/state_controller.h
@@ -150,9 +150,12 @@
   void HandleUserActivity();
   void HandleVideoActivity();
 
-  // Handle audio activity starting or stopping.
+  // Handles audio activity starting or stopping.
   void HandleAudioStateChange(bool active);
 
+  // Handles updates to the TPM status.
+  void HandleTpmStatus(int dictionary_attack_count);
+
   // PrefsInterface::Observer implementation:
   void OnPrefChanged(const std::string& pref_name) override;
 
@@ -367,6 +370,14 @@
   // Should |policy_| be ignored?  Used by tests and developers.
   bool ignore_external_policy_;
 
+  // TPM dictionary-attack counter value.
+  int tpm_dictionary_attack_count_;
+
+  // |tpm_dictionary_attack_count_| value at or above which the system will
+  // suspend instead of shutting down in some cases (see
+  // http://crbug.com/462428), or 0 if disabled.
+  int tpm_dictionary_attack_suspend_threshold_;
+
   base::TimeTicks last_user_activity_time_;
   base::TimeTicks last_video_activity_time_;
 
diff --git a/power_manager/powerd/policy/state_controller_unittest.cc b/power_manager/powerd/policy/state_controller_unittest.cc
index ba481ec..73df480 100644
--- a/power_manager/powerd/policy/state_controller_unittest.cc
+++ b/power_manager/powerd/policy/state_controller_unittest.cc
@@ -1529,5 +1529,64 @@
   ASSERT_TRUE(AdvanceTimeAndTriggerTimeout(kWarningDelay));
 }
 
+// Tests that idle and lid-closed "shut down" actions are overridden to instead
+// suspend when the TPM dictionary-attack count is high.
+TEST_F(StateControllerTest, SuspendInsteadOfShuttingDownForTpmCounter) {
+  const base::TimeDelta kIdleDelay = base::TimeDelta::FromSeconds(300);
+  initial_policy_.mutable_ac_delays()->set_screen_dim_ms(0);
+  initial_policy_.mutable_ac_delays()->set_screen_off_ms(0);
+  initial_policy_.mutable_ac_delays()->set_screen_lock_ms(0);
+  initial_policy_.mutable_ac_delays()->set_idle_ms(kIdleDelay.InMilliseconds());
+  initial_policy_.set_ac_idle_action(PowerManagementPolicy_Action_SHUT_DOWN);
+  initial_policy_.set_lid_closed_action(PowerManagementPolicy_Action_SHUT_DOWN);
+
+  const int kThreshold = 10;
+  prefs_.SetInt64(kTpmCounterSuspendThresholdPref, kThreshold);
+  Init();
+
+  // With the count below the threshold, the "shut down" lid-closed action
+  // should be honored.
+  controller_.HandleTpmStatus(kThreshold - 1);
+  delegate_.set_lid_state(LID_CLOSED);
+  controller_.HandleLidStateChange(LID_CLOSED);
+  EXPECT_EQ(kShutDown, delegate_.GetActions());
+  delegate_.set_lid_state(LID_OPEN);
+  controller_.HandleLidStateChange(LID_OPEN);
+
+  // Ditto for the idle action.
+  EXPECT_TRUE(AdvanceTimeAndTriggerTimeout(kIdleDelay));
+  EXPECT_EQ(kShutDown, delegate_.GetActions());
+  controller_.HandleUserActivity();
+
+  // If the count reaches the threshold, the system should suspend instead of
+  // shutting down.
+  controller_.HandleTpmStatus(kThreshold);
+  delegate_.set_lid_state(LID_CLOSED);
+  controller_.HandleLidStateChange(LID_CLOSED);
+  EXPECT_EQ(kSuspend, delegate_.GetActions());
+  delegate_.set_lid_state(LID_OPEN);
+  controller_.HandleLidStateChange(LID_OPEN);
+
+  EXPECT_TRUE(AdvanceTimeAndTriggerTimeout(kIdleDelay));
+  EXPECT_EQ(kSuspend, delegate_.GetActions());
+  controller_.HandleUserActivity();
+
+  // If non-"shut down" actions are set, they shouldn't be overridden.
+  initial_policy_.set_ac_idle_action(PowerManagementPolicy_Action_DO_NOTHING);
+  initial_policy_.set_lid_closed_action(
+      PowerManagementPolicy_Action_STOP_SESSION);
+  controller_.HandlePolicyChange(initial_policy_);
+
+  delegate_.set_lid_state(LID_CLOSED);
+  controller_.HandleLidStateChange(LID_CLOSED);
+  EXPECT_EQ(kStopSession, delegate_.GetActions());
+  delegate_.set_lid_state(LID_OPEN);
+  controller_.HandleLidStateChange(LID_OPEN);
+
+  EXPECT_TRUE(AdvanceTimeAndTriggerTimeout(kIdleDelay));
+  EXPECT_EQ(kNoActions, delegate_.GetActions());
+  controller_.HandleUserActivity();
+}
+
 }  // namespace policy
 }  // namespace power_manager