text: // Copyright 2014 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  #include "google_apis/gcm/engine/heartbeat_manager.h"
  
  #include <utility>
  
  #include "base/callback.h"
  #include "base/location.h"
  #include "base/metrics/histogram_macros.h"
  #include "base/power_monitor/power_monitor.h"
  #include "base/threading/thread_task_runner_handle.h"
  #include "base/time/time.h"
  #include "base/timer/timer.h"
  #include "build/build_config.h"
  #include "google_apis/gcm/protocol/mcs.pb.h"
  #include "net/base/network_change_notifier.h"
  
  namespace gcm {
  
  namespace {
  // The default heartbeat when on a mobile or unknown network .
  const int kCellHeartbeatDefaultMs = 1000 * 60 * 28;  // 28 minutes.
  // The default heartbeat when on WiFi (also used for ethernet).
  const int kWifiHeartbeatDefaultMs = 1000 * 60 * 15;  // 15 minutes.
  // The default heartbeat ack interval.
  const int kHeartbeatAckDefaultMs = 1000 * 60 * 1;  // 1 minute.
  // Minimum allowed client default heartbeat interval.
  const int kMinClientHeartbeatIntervalMs = 1000 * 30;  // 30 seconds.
  // Minimum time spent sleeping before we force a new heartbeat.
  const int kMinSuspendTimeMs = 1000 * 10; // 10 seconds.
  
  #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
  // The period at which to check if the heartbeat time has passed. Used to
  // protect against platforms where the timer is delayed by the system being
  // suspended.  Only needed on linux because the other OSes provide a standard
  // way to be notified of system suspend and resume events.
  const int kHeartbeatMissedCheckMs = 1000 * 60 * 5;  // 5 minutes.
  #endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)
  
  }  // namespace
  
  HeartbeatManager::HeartbeatManager()
      : waiting_for_ack_(false),
        heartbeat_interval_ms_(0),
        server_interval_ms_(0),
        client_interval_ms_(0),
        heartbeat_timer_(new base::Timer(true /* retain_user_task */,
                                         false /* is_repeating */)),
        weak_ptr_factory_(this) {
  }
  
  HeartbeatManager::~HeartbeatManager() {
    // Stop listening for system suspend and resume events.
    base::PowerMonitor* monitor = base::PowerMonitor::Get();
    if (monitor)
      monitor->RemoveObserver(this);
  }
  
  void HeartbeatManager::Start(
      const base::Closure& send_heartbeat_callback,
      const ReconnectCallback& trigger_reconnect_callback) {
    DCHECK(!send_heartbeat_callback.is_null());
    DCHECK(!trigger_reconnect_callback.is_null());
    send_heartbeat_callback_ = send_heartbeat_callback;
    trigger_reconnect_callback_ = trigger_reconnect_callback;
  
    // Listen for system suspend and resume events.
    base::PowerMonitor* monitor = base::PowerMonitor::Get();
    if (monitor)
      monitor->AddObserver(this);
  
    // Calculated the heartbeat interval just before we start the timer.
    UpdateHeartbeatInterval();
  
    // Kicks off the timer.
    waiting_for_ack_ = false;
    RestartTimer();
  }
  
  void HeartbeatManager::Stop() {
    heartbeat_expected_time_ = base::Time();
    heartbeat_interval_ms_ = 0;
    heartbeat_timer_->Stop();
    waiting_for_ack_ = false;
  
    base::PowerMonitor* monitor = base::PowerMonitor::Get();
    if (monitor)
      monitor->RemoveObserver(this);
  }
  
  void HeartbeatManager::OnHeartbeatAcked() {
    if (!heartbeat_timer_->IsRunning())
      return;
  
    DCHECK(!send_heartbeat_callback_.is_null());
    DCHECK(!trigger_reconnect_callback_.is_null());
    waiting_for_ack_ = false;
    RestartTimer();
  }
  
  void HeartbeatManager::UpdateHeartbeatConfig(
      const mcs_proto::HeartbeatConfig& config) {
    if (!config.IsInitialized() ||
        !config.has_interval_ms() ||
        config.interval_ms() <= 0) {
      return;
    }
    DVLOG(1) << "Updating server heartbeat interval to " << config.interval_ms();
    server_interval_ms_ = config.interval_ms();
  
    // Make sure heartbeat interval is recalculated when new server interval is
    // available.
    UpdateHeartbeatInterval();
  }
  
  base::TimeTicks HeartbeatManager::GetNextHeartbeatTime() const {
    if (heartbeat_timer_->IsRunning())
      return heartbeat_timer_->desired_run_time();
    else
      return base::TimeTicks();
  }
  
  void HeartbeatManager::UpdateHeartbeatTimer(
      std::unique_ptr<base::Timer> timer) {
    bool was_running = heartbeat_timer_->IsRunning();
    base::TimeDelta remaining_delay =
        heartbeat_timer_->desired_run_time() - base::TimeTicks::Now();
    base::Closure timer_task(heartbeat_timer_->user_task());
  
    heartbeat_timer_->Stop();
    heartbeat_timer_ = std::move(timer);
  
    if (was_running)
      heartbeat_timer_->Start(FROM_HERE, remaining_delay, timer_task);
  }
  
  void HeartbeatManager::OnSuspend() {
    // The system is going to sleep. Record the time, so on resume we know how
    // much time the machine was suspended.
    suspend_time_ = base::Time::Now();
  }
  
  void HeartbeatManager::OnResume() {
    // The system just resumed from sleep. It's likely that the connection to
    // MCS was silently lost during that time, even if a heartbeat is not yet
    // due. Force a heartbeat to detect if the connection is still good.
    base::TimeDelta elapsed = base::Time::Now() - suspend_time_;
    UMA_HISTOGRAM_LONG_TIMES("GCM.SuspendTime", elapsed);
  
    // Make sure a minimum amount of time has passed before forcing a heartbeat to
    // avoid any tight loop scenarios.
    // If the |send_heartbeat_callback_| is null, it means the heartbeat manager
    // hasn't been started, so do nothing.
    if (elapsed > base::TimeDelta::FromMilliseconds(kMinSuspendTimeMs) &&
        !send_heartbeat_callback_.is_null())
      OnHeartbeatTriggered();
  }
  
  void HeartbeatManager::OnHeartbeatTriggered() {
    // Reset the weak pointers used for heartbeat checks.
    weak_ptr_factory_.InvalidateWeakPtrs();
  
    if (waiting_for_ack_) {
      LOG(WARNING) << "Lost connection to MCS, reconnecting.";
      ResetConnection(ConnectionFactory::HEARTBEAT_FAILURE);
      return;
    }
  
    waiting_for_ack_ = true;
    RestartTimer();
    send_heartbeat_callback_.Run();
  }
  
  void HeartbeatManager::RestartTimer() {
    int interval_ms = heartbeat_interval_ms_;
    if (waiting_for_ack_) {
      interval_ms = kHeartbeatAckDefaultMs;
      DVLOG(1) << "Resetting timer for ack within " << interval_ms << " ms.";
    } else {
      DVLOG(1) << "Sending next heartbeat in " << interval_ms << " ms.";
    }
  
    heartbeat_expected_time_ =
        base::Time::Now() + base::TimeDelta::FromMilliseconds(interval_ms);
    heartbeat_timer_->Start(FROM_HERE,
                            base::TimeDelta::FromMilliseconds(interval_ms),
                            base::Bind(&HeartbeatManager::OnHeartbeatTriggered,
                                       weak_ptr_factory_.GetWeakPtr()));
  
  #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
    // Windows, Mac, Android, iOS, and Chrome OS all provide a way to be notified
    // when the system is suspending or resuming.  The only one that does not is
    // Linux so we need to poll to check for missed heartbeats.
    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&HeartbeatManager::CheckForMissedHeartbeat,
                   weak_ptr_factory_.GetWeakPtr()),
        base::TimeDelta::FromMilliseconds(kHeartbeatMissedCheckMs));
  #endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)
  }
  
  void HeartbeatManager::CheckForMissedHeartbeat() {
    // If there's no heartbeat pending, return without doing anything.
    if (heartbeat_expected_time_.is_null())
      return;
  
    // If the heartbeat has been missed, manually trigger it.
    if (base::Time::Now() > heartbeat_expected_time_) {
      UMA_HISTOGRAM_LONG_TIMES("GCM.HeartbeatMissedDelta",
                               base::Time::Now() - heartbeat_expected_time_);
      OnHeartbeatTriggered();
      return;
    }
  
  #if defined(OS_LINUX) && !defined(OS_CHROMEOS)
    // Otherwise check again later.
    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&HeartbeatManager::CheckForMissedHeartbeat,
                   weak_ptr_factory_.GetWeakPtr()),
        base::TimeDelta::FromMilliseconds(kHeartbeatMissedCheckMs));
  #endif  // defined(OS_LINUX) && !defined(OS_CHROMEOS)
  }
  
  void HeartbeatManager::UpdateHeartbeatInterval() {
    // Server interval takes precedence over client interval, even if the latter
    // is less.
    if (server_interval_ms_ != 0) {
      // If a server interval is set, it overrides any local one.
      heartbeat_interval_ms_ = server_interval_ms_;
    } else if (HasClientHeartbeatInterval() &&
               (client_interval_ms_ < heartbeat_interval_ms_ ||
                heartbeat_interval_ms_ == 0)) {
      // Client interval might have been adjusted up, which should only take
      // effect during a reconnection.
      heartbeat_interval_ms_ = client_interval_ms_;
    } else if (heartbeat_interval_ms_ == 0) {
      // If interval is still 0, recalculate it based on network type.
      heartbeat_interval_ms_ = GetDefaultHeartbeatInterval();
    }
    DCHECK_GT(heartbeat_interval_ms_, 0);
  }
  
  int HeartbeatManager::GetDefaultHeartbeatInterval() {
    // For unknown connections, use the longer cellular heartbeat interval.
    int heartbeat_interval_ms = kCellHeartbeatDefaultMs;
    if (net::NetworkChangeNotifier::GetConnectionType() ==
            net::NetworkChangeNotifier::CONNECTION_WIFI ||
        net::NetworkChangeNotifier::GetConnectionType() ==
            net::NetworkChangeNotifier::CONNECTION_ETHERNET) {
      heartbeat_interval_ms = kWifiHeartbeatDefaultMs;
    }
    return heartbeat_interval_ms;
  }
  
  int HeartbeatManager::GetMaxClientHeartbeatIntervalMs() {
    return GetDefaultHeartbeatInterval();
  }
  
  int HeartbeatManager::GetMinClientHeartbeatIntervalMs() {
    // Returning a constant. This should be adjusted for connection type, like the
    // default/max interval.
    return kMinClientHeartbeatIntervalMs;
  }
  
  void HeartbeatManager::SetClientHeartbeatIntervalMs(int interval_ms) {
    if ((interval_ms != 0 && !IsValidClientHeartbeatInterval(interval_ms)) ||
        interval_ms == client_interval_ms_) {
      return;
    }
  
    client_interval_ms_ = interval_ms;
    // Only reset connection if the new heartbeat interval is shorter. If it is
    // longer, the connection will reset itself at some point and interval will be
    // fixed.
    if (client_interval_ms_ > 0 && client_interval_ms_ < heartbeat_interval_ms_) {
      ResetConnection(ConnectionFactory::NEW_HEARTBEAT_INTERVAL);
    }
  }
  
  int HeartbeatManager::GetClientHeartbeatIntervalMs() {
    return client_interval_ms_;
  }
  
  bool HeartbeatManager::HasClientHeartbeatInterval() {
    return client_interval_ms_ != 0;
  }
  
  bool HeartbeatManager::IsValidClientHeartbeatInterval(int interval) {
    int max_heartbeat_interval = GetDefaultHeartbeatInterval();
    return kMinClientHeartbeatIntervalMs <= interval &&
        interval <= max_heartbeat_interval;
  }
  
  void HeartbeatManager::ResetConnection(
      ConnectionFactory::ConnectionResetReason reason) {
    Stop();
    trigger_reconnect_callback_.Run(reason);
  }
  
  }  // namespace gcm
  