// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/arc/idle_manager/arc_idle_manager.h"

#include "ash/shell.h"
#include "base/logging.h"
#include "base/metrics/histogram_functions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/arc/idle_manager/arc_background_service_observer.h"
#include "chrome/browser/ash/arc/idle_manager/arc_cpu_throttle_observer.h"
#include "chrome/browser/ash/arc/idle_manager/arc_display_power_observer.h"
#include "chrome/browser/ash/arc/idle_manager/arc_on_battery_observer.h"
#include "chrome/browser/ash/arc/idle_manager/arc_window_observer.h"
#include "chromeos/ash/components/dbus/patchpanel/patchpanel_client.h"
#include "chromeos/ash/experiences/arc/arc_browser_context_keyed_service_factory_base.h"
#include "chromeos/ash/experiences/arc/mojom/power.mojom.h"
#include "chromeos/ash/experiences/arc/session/arc_bridge_service.h"

namespace arc {

namespace {

// Constant value for idle-manager's "enable_delay_".
constexpr base::TimeDelta kArcIdleManagerDelay = base::Seconds(360);

class DefaultDelegateImpl : public ArcIdleManager::Delegate {
 public:
  DefaultDelegateImpl() = default;

  DefaultDelegateImpl(const DefaultDelegateImpl&) = delete;
  DefaultDelegateImpl& operator=(const DefaultDelegateImpl&) = delete;

  ~DefaultDelegateImpl() override = default;

  // ArcIdleManager::Delegate:
  void SetIdleState(ArcPowerBridge* arc_power_bridge,
                    ArcBridgeService* bridge,
                    bool enable) override {
    if (!arc_power_bridge) {
      return;
    }
    arc_power_bridge->NotifyAndroidIdleState(
        bridge, enable ? arc::mojom::IdleState::ACTIVE
                       : arc::mojom::IdleState::INACTIVE);
  }
};

// Singleton factory for ArcIdleManager.
class ArcIdleManagerFactory
    : public internal::ArcBrowserContextKeyedServiceFactoryBase<
          ArcIdleManager,
          ArcIdleManagerFactory> {
 public:
  static constexpr const char* kName = "ArcIdleManagerFactory";
  static ArcIdleManagerFactory* GetInstance() {
    static base::NoDestructor<ArcIdleManagerFactory> instance;
    return instance.get();
  }

 private:
  friend class base::NoDestructor<ArcIdleManagerFactory>;

  ArcIdleManagerFactory() { DependsOn(ArcPowerBridgeFactory::GetInstance()); }
  ~ArcIdleManagerFactory() override = default;
};

}  // namespace

// static
ArcIdleManager* ArcIdleManager::GetForBrowserContext(
    content::BrowserContext* context) {
  return ArcIdleManagerFactory::GetForBrowserContext(context);
}

// static
ArcIdleManager* ArcIdleManager::GetForBrowserContextForTesting(
    content::BrowserContext* context) {
  return ArcIdleManagerFactory::GetForBrowserContextForTesting(context);
}

ArcIdleManager::ArcIdleManager(content::BrowserContext* context,
                               ArcBridgeService* bridge)
    : ThrottleService(context),
      delegate_(std::make_unique<DefaultDelegateImpl>()),
      bridge_(bridge) {
  AddObserver(std::make_unique<ArcCpuThrottleObserver>());
  AddObserver(std::make_unique<ArcBackgroundServiceObserver>());
  AddObserver(std::make_unique<ArcWindowObserver>());
  // For test purposes, ignore battery status changes, allowing Doze mode to
  // kick in even if we do not receive powerd changes related to battery.
  LOG(WARNING) << "Doze will be enabled regardless of battery status";
  AddObserver(std::make_unique<ArcDisplayPowerObserver>());

  enable_delay_ = kArcIdleManagerDelay;

  arc_power_bridge_ = ArcPowerBridge::GetForBrowserContext(context);

  // This maybe null in unit tests.
  if (arc_power_bridge_) {
    arc_power_bridge_->DisableAndroidIdleControl();
    powerbridge_observation_.Observe(arc_power_bridge_);
  }

  DCHECK(bridge_);
  bridge_->power()->AddObserver(this);
  DETACH_FROM_SEQUENCE(sequence_checker_);
}

// Destructor is empty because this is a KeyedService, which must
// cleanup in Shutdown();
ArcIdleManager::~ArcIdleManager() = default;

// static
void ArcIdleManager::EnsureFactoryBuilt() {
  ArcIdleManagerFactory::GetInstance();
}

void ArcIdleManager::Shutdown() {
  // After this is done, we will no longer get connection notifications.
  bridge_->power()->RemoveObserver(this);

  // No more notifications about VM resumed.
  powerbridge_observation_.Reset();

  // Won't hear about display power changes anymore.
  display_observation_.Reset();

  // Safeguard against resource leak by observers.
  OnConnectionClosed();
}

void ArcIdleManager::OnConnectionReady() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (is_connected_)
    return;
  StartObservers();

  // ash::Shell may not exist in tests.
  if (ash::Shell::HasInstance()) {
    display_observation_.Observe(ash::Shell::Get()->display_configurator());
  }

  delegate_->SetIdleState(arc_power_bridge_, bridge_, !should_throttle());
  is_connected_ = true;

  // Always reset the timer on connect.
  LogScreenOffTimer(/*toggle_timer*/ true);
  // Next call to LogScreenOffTimer from ThrottleInstance will either:
  //   a) throttle=true: reset the timer again - and that's fine.
  //   b) throttle=false: log time between connect and un-throttle.
}

void ArcIdleManager::OnConnectionClosed() {
  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
  if (!is_connected_)
    return;

  display_observation_.Reset();

  StopObservers();
  if (should_throttle()) {
    // Maybe a logout, or a systemserver crash.
    // Either way, we stop tracking and log.
    LogScreenOffTimer(/*toggle_timer*/ false);
  }
  is_connected_ = false;
}

void ArcIdleManager::OnPowerStateChanged(
    chromeos::DisplayPowerState power_state) {
  if (power_state == chromeos::DISPLAY_POWER_ALL_OFF) {
    // Display is OFF.
    enable_delay_ = base::TimeDelta();  // No more new timers.
    if (enable_timer_.IsRunning()) {
      enable_timer_.Stop();  // Doze sooner than scheduled.
      RequestDoze(true);
    }
  } else {
    // Display is ON.
    enable_delay_ = kArcIdleManagerDelay;
  }
}

void ArcIdleManager::ThrottleInstance(bool should_throttle) {
  // Note: this never happens in between StopObservers() - StartObservers();
  if (!first_idle_happened_ && !should_throttle) {
    // Both the ArcIdleManager and Android start life as un-throttled (not
    // idle). Until it's time to throttle Android, the state is aligned, and
    // there's no need to send requests to change state.
    return;
  }
  first_idle_happened_ = true;
  if (should_throttle) {
    // Enable Doze mode. May need to postpone the request.
    if (!enable_delay_.is_zero()) {
      enable_timer_.Start(FROM_HERE, enable_delay_,
                          base::BindOnce(&ArcIdleManager::RequestDoze,
                                         weak_ptr_factory_.GetWeakPtr(), true));
    } else {
      RequestDoze(true);
    }
  } else {
    bool is_running = enable_timer_.IsRunning();
    enable_timer_.Stop();
    if (!is_running) {
      // Disable Doze mode should execute immediately, otherwise app launch may
      // be blocked.
      RequestDoze(false);
    }
    // else, we had a scheduled timer to go idle, and we canceled it (i.e., we
    // are still in active state), and we are not configured to force
    // reactivation. So no need to request a wake up.
  }
}

void ArcIdleManager::OnVmResumed() {
  if (!should_throttle()) {
    // A resume happens because there was a prior suspend.
    // That earlier suspend counts as first-idle.
    first_idle_happened_ = true;

    // Just sync up Android state with internal state.
    // No need for logging metrics, not a state change.
    RequestDozeWithoutMetrics(false);
  }
}

void ArcIdleManager::OnWillDestroyArcPowerBridge() {
  // No more notifications about VM resumed.
  powerbridge_observation_.Reset();
  arc_power_bridge_ = nullptr;
}

void ArcIdleManager::LogScreenOffTimer(bool toggle_timer) {
  if (toggle_timer) {
    // Start measuring now.
    interactive_off_span_timer_ = base::ElapsedTimer();
  } else {
    base::TimeDelta elapsed = interactive_off_span_timer_.Elapsed();
    // Report time spent with screen-off, in milliseconds. Use 100 buckets,
    // as the span of allowed values is very wide (1ms -> 8h(28,800,000ms)).
    // Notice that the very first call to this function may hit this case,
    // which will cause us to log the time between start-up and the
    // transition to no-throttle (first-active), which is an appropriate
    // measurement value.
    base::UmaHistogramCustomTimes("Arc.IdleManager.ScreenOffTime",
                                  /*sample=*/elapsed,
                                  /*min=*/base::Milliseconds(1),
                                  /*max=*/base::Hours(8), /*buckets=*/100);
  }
}

void ArcIdleManager::RequestDozeWithoutMetrics(bool enabled) {
  delegate_->SetIdleState(arc_power_bridge_, bridge_, !enabled);
}

void ArcIdleManager::RequestDoze(bool enabled) {
  LogScreenOffTimer(/*toggle_timer*/ enabled);
  RequestDozeWithoutMetrics(enabled);
}

}  // namespace arc
