// 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 "chrome/browser/signin/easy_unlock_service.h"

#include "base/bind.h"
#include "base/command_line.h"
#include "base/logging.h"
#include "base/metrics/field_trial.h"
#include "base/prefs/pref_registry_simple.h"
#include "base/prefs/pref_service.h"
#include "base/prefs/scoped_user_pref_update.h"
#include "base/thread_task_runner_handle.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/extensions/component_loader.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/easy_unlock_auth_attempt.h"
#include "chrome/browser/signin/easy_unlock_service_factory.h"
#include "chrome/browser/signin/easy_unlock_service_observer.h"
#include "chrome/browser/signin/screenlock_bridge.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/extensions/api/easy_unlock_private.h"
#include "chrome/common/extensions/extension_constants.h"
#include "chrome/common/pref_names.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/user_manager/user.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"
#include "extensions/browser/event_router.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extension_system.h"
#include "extensions/common/one_shot_event.h"
#include "grit/browser_resources.h"

#if defined(OS_CHROMEOS)
#include "chrome/browser/chromeos/login/easy_unlock/easy_unlock_key_manager.h"
#include "chrome/browser/chromeos/login/session/user_session_manager.h"
#include "chrome/browser/chromeos/profiles/profile_helper.h"
#include "chromeos/chromeos_switches.h"
#include "chromeos/dbus/dbus_thread_manager.h"
#include "chromeos/dbus/power_manager_client.h"
#endif

namespace {

extensions::ComponentLoader* GetComponentLoader(
    content::BrowserContext* context) {
  extensions::ExtensionSystem* extension_system =
      extensions::ExtensionSystem::Get(context);
  ExtensionService* extension_service = extension_system->extension_service();
  return extension_service->component_loader();
}

PrefService* GetLocalState() {
  return g_browser_process ? g_browser_process->local_state() : NULL;
}

}  // namespace

// static
EasyUnlockService* EasyUnlockService::Get(Profile* profile) {
  return EasyUnlockServiceFactory::GetForProfile(profile);
}

// static
EasyUnlockService* EasyUnlockService::GetForUser(
    const user_manager::User& user) {
#if defined(OS_CHROMEOS)
  Profile* profile = chromeos::ProfileHelper::Get()->GetProfileByUser(&user);
  if (!profile)
    return NULL;
  return EasyUnlockService::Get(profile);
#else
  return NULL;
#endif
}

// static
bool EasyUnlockService::IsSignInEnabled() {
#if defined(OS_CHROMEOS)
  const std::string group_name =
      base::FieldTrialList::FindFullName("EasySignIn");

  if (CommandLine::ForCurrentProcess()->HasSwitch(
          chromeos::switches::kDisableEasySignin)) {
    return false;
  }

  return group_name == "Enable";
#else
  return false;
#endif
}

class EasyUnlockService::BluetoothDetector
    : public device::BluetoothAdapter::Observer {
 public:
  explicit BluetoothDetector(EasyUnlockService* service)
      : service_(service),
        weak_ptr_factory_(this) {
  }

  virtual ~BluetoothDetector() {
    if (adapter_.get())
      adapter_->RemoveObserver(this);
  }

  void Initialize() {
    if (!device::BluetoothAdapterFactory::IsBluetoothAdapterAvailable())
      return;

    device::BluetoothAdapterFactory::GetAdapter(
        base::Bind(&BluetoothDetector::OnAdapterInitialized,
                   weak_ptr_factory_.GetWeakPtr()));
  }

  bool IsPresent() const { return adapter_.get() && adapter_->IsPresent(); }

  // device::BluetoothAdapter::Observer:
  virtual void AdapterPresentChanged(device::BluetoothAdapter* adapter,
                                     bool present) OVERRIDE {
    service_->OnBluetoothAdapterPresentChanged();
  }

 private:
  void OnAdapterInitialized(scoped_refptr<device::BluetoothAdapter> adapter) {
    adapter_ = adapter;
    adapter_->AddObserver(this);
    service_->OnBluetoothAdapterPresentChanged();
  }

  // Owner of this class and should out-live this class.
  EasyUnlockService* service_;
  scoped_refptr<device::BluetoothAdapter> adapter_;
  base::WeakPtrFactory<BluetoothDetector> weak_ptr_factory_;

  DISALLOW_COPY_AND_ASSIGN(BluetoothDetector);
};

#if defined(OS_CHROMEOS)
class EasyUnlockService::PowerMonitor
    : public chromeos::PowerManagerClient::Observer {
 public:
  explicit PowerMonitor(EasyUnlockService* service)
      : service_(service),
        waking_up_(false),
        weak_ptr_factory_(this) {
    chromeos::DBusThreadManager::Get()->GetPowerManagerClient()->
        AddObserver(this);
  }

  virtual ~PowerMonitor() {
    chromeos::DBusThreadManager::Get()->GetPowerManagerClient()->
        RemoveObserver(this);
  }

  bool waking_up() const { return waking_up_; }

 private:
  // chromeos::PowerManagerClient::Observer:
  virtual void SuspendImminent() OVERRIDE {
    service_->PrepareForSuspend();
  }

  virtual void SuspendDone(const base::TimeDelta& sleep_duration) OVERRIDE {
    waking_up_ = true;
    base::ThreadTaskRunnerHandle::Get()->PostDelayedTask(
        FROM_HERE,
        base::Bind(&PowerMonitor::ResetWakingUp,
                   weak_ptr_factory_.GetWeakPtr()),
        base::TimeDelta::FromSeconds(5));
    service_->UpdateAppState();
    // Note that |this| may get deleted after |UpdateAppState| is called.
  }

  void ResetWakingUp() {
    waking_up_ = false;
    service_->UpdateAppState();
  }

  EasyUnlockService* service_;
  bool waking_up_;
  base::WeakPtrFactory<PowerMonitor> weak_ptr_factory_;

  DISALLOW_COPY_AND_ASSIGN(PowerMonitor);
};
#endif

EasyUnlockService::EasyUnlockService(Profile* profile)
    : profile_(profile),
      bluetooth_detector_(new BluetoothDetector(this)),
      shut_down_(false),
      weak_ptr_factory_(this) {
  extensions::ExtensionSystem::Get(profile_)->ready().Post(
      FROM_HERE,
      base::Bind(&EasyUnlockService::Initialize,
                 weak_ptr_factory_.GetWeakPtr()));
}

EasyUnlockService::~EasyUnlockService() {
}

// static
void EasyUnlockService::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  registry->RegisterBooleanPref(
      prefs::kEasyUnlockEnabled,
      false,
      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
  registry->RegisterDictionaryPref(
      prefs::kEasyUnlockPairing,
      new base::DictionaryValue(),
      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
  registry->RegisterBooleanPref(
      prefs::kEasyUnlockAllowed,
      true,
      user_prefs::PrefRegistrySyncable::UNSYNCABLE_PREF);
}

// static
void EasyUnlockService::RegisterPrefs(PrefRegistrySimple* registry) {
  registry->RegisterDictionaryPref(prefs::kEasyUnlockHardlockState);
}

// static
void EasyUnlockService::ResetLocalStateForUser(const std::string& user_id) {
  DCHECK(!user_id.empty());

  PrefService* local_state = GetLocalState();
  if (!local_state)
    return;

  DictionaryPrefUpdate update(local_state, prefs::kEasyUnlockHardlockState);
  update->RemoveWithoutPathExpansion(user_id, NULL);
}

bool EasyUnlockService::IsAllowed() {
  if (shut_down_)
    return false;

  if (!IsAllowedInternal())
    return false;

#if defined(OS_CHROMEOS)
  if (!bluetooth_detector_->IsPresent())
    return false;

  return true;
#else
  // TODO(xiyuan): Revisit when non-chromeos platforms are supported.
  return false;
#endif
}

void EasyUnlockService::SetHardlockState(
    EasyUnlockScreenlockStateHandler::HardlockState state) {
  const std::string user_id = GetUserEmail();
  if (user_id.empty())
    return;

  SetHardlockStateForUser(user_id, state);
}

EasyUnlockScreenlockStateHandler::HardlockState
EasyUnlockService::GetHardlockState() const {
  EasyUnlockScreenlockStateHandler::HardlockState state;
  if (GetPersistedHardlockState(&state))
    return state;

  return EasyUnlockScreenlockStateHandler::NO_HARDLOCK;
}

bool EasyUnlockService::GetPersistedHardlockState(
    EasyUnlockScreenlockStateHandler::HardlockState* state) const {
  std::string user_id = GetUserEmail();
  if (user_id.empty())
    return false;

  PrefService* local_state = GetLocalState();
  if (!local_state)
    return false;

  const base::DictionaryValue* dict =
      local_state->GetDictionary(prefs::kEasyUnlockHardlockState);
  int state_int;
  if (dict && dict->GetIntegerWithoutPathExpansion(user_id, &state_int)) {
    *state =
        static_cast<EasyUnlockScreenlockStateHandler::HardlockState>(state_int);
    return true;
  }

  return false;
}

void EasyUnlockService::ShowInitialUserState() {
  if (!GetScreenlockStateHandler())
    return;

  EasyUnlockScreenlockStateHandler::HardlockState state;
  bool has_persisted_state = GetPersistedHardlockState(&state);
  if (!has_persisted_state)
    return;

  if (state == EasyUnlockScreenlockStateHandler::NO_HARDLOCK) {
    // Show connecting icon early when there is a persisted non hardlock state.
    UpdateScreenlockState(
        EasyUnlockScreenlockStateHandler::STATE_BLUETOOTH_CONNECTING);
  } else {
    screenlock_state_handler_->MaybeShowHardlockUI();
  }
}

EasyUnlockScreenlockStateHandler*
    EasyUnlockService::GetScreenlockStateHandler() {
  if (!IsAllowed())
    return NULL;
  if (!screenlock_state_handler_) {
    screenlock_state_handler_.reset(new EasyUnlockScreenlockStateHandler(
        GetUserEmail(),
        GetHardlockState(),
        ScreenlockBridge::Get()));
  }
  return screenlock_state_handler_.get();
}

bool EasyUnlockService::UpdateScreenlockState(
    EasyUnlockScreenlockStateHandler::State state) {
  EasyUnlockScreenlockStateHandler* handler = GetScreenlockStateHandler();
  if (!handler)
    return false;

  handler->ChangeState(state);

  if (state != EasyUnlockScreenlockStateHandler::STATE_AUTHENTICATED)
    auth_attempt_.reset();
  return true;
}

void EasyUnlockService::AttemptAuth(const std::string& user_id) {
  auth_attempt_.reset(new EasyUnlockAuthAttempt(
      profile_,
      GetUserEmail(),
      GetType() == TYPE_REGULAR ? EasyUnlockAuthAttempt::TYPE_UNLOCK
                                : EasyUnlockAuthAttempt::TYPE_SIGNIN));
  if (!auth_attempt_->Start(user_id))
    auth_attempt_.reset();
}

void EasyUnlockService::FinalizeUnlock(bool success) {
  if (auth_attempt_)
    auth_attempt_->FinalizeUnlock(GetUserEmail(), success);
  auth_attempt_.reset();
}

void EasyUnlockService::FinalizeSignin(const std::string& key) {
  if (!auth_attempt_)
    return;
  std::string wrapped_secret = GetWrappedSecret();
  if (!wrapped_secret.empty())
    auth_attempt_->FinalizeSignin(GetUserEmail(), wrapped_secret, key);
  auth_attempt_.reset();
}

void EasyUnlockService::CheckCryptohomeKeysAndMaybeHardlock() {
#if defined(OS_CHROMEOS)
  std::string user_id = GetUserEmail();
  if (user_id.empty())
    return;

  const base::ListValue* device_list = GetRemoteDevices();
  std::set<std::string> paired_devices;
  if (device_list) {
    chromeos::EasyUnlockDeviceKeyDataList parsed_paired;
    chromeos::EasyUnlockKeyManager::RemoteDeviceListToDeviceDataList(
        *device_list, &parsed_paired);
    for (const auto& device_key_data : parsed_paired)
      paired_devices.insert(device_key_data.psk);
  }
  if (paired_devices.empty()) {
    SetHardlockState(EasyUnlockScreenlockStateHandler::NO_PAIRING);
    return;
  }

  // No need to compare if a change is already recorded.
  if (GetHardlockState() == EasyUnlockScreenlockStateHandler::PAIRING_CHANGED ||
      GetHardlockState() == EasyUnlockScreenlockStateHandler::PAIRING_ADDED) {
    return;
  }

  chromeos::EasyUnlockKeyManager* key_manager =
      chromeos::UserSessionManager::GetInstance()->GetEasyUnlockKeyManager();
  DCHECK(key_manager);

  key_manager->GetDeviceDataList(
      chromeos::UserContext(user_id),
      base::Bind(&EasyUnlockService::OnCryptohomeKeysFetchedForChecking,
                 weak_ptr_factory_.GetWeakPtr(),
                 user_id,
                 paired_devices));
#endif
}

void EasyUnlockService::SetTrialRun() {
  DCHECK(GetType() == TYPE_REGULAR);

  EasyUnlockScreenlockStateHandler* handler = GetScreenlockStateHandler();
  if (handler)
    handler->SetTrialRun();
}

void EasyUnlockService::AddObserver(EasyUnlockServiceObserver* observer) {
  observers_.AddObserver(observer);
}

void EasyUnlockService::RemoveObserver(EasyUnlockServiceObserver* observer) {
  observers_.RemoveObserver(observer);
}

void  EasyUnlockService::Shutdown() {
  if (shut_down_)
    return;
  shut_down_ = true;

  ShutdownInternal();

  weak_ptr_factory_.InvalidateWeakPtrs();

  ResetScreenlockState();
  bluetooth_detector_.reset();
#if defined(OS_CHROMEOS)
  power_monitor_.reset();
#endif
}

void EasyUnlockService::LoadApp() {
  DCHECK(IsAllowed());

#if defined(GOOGLE_CHROME_BUILD)
  base::FilePath easy_unlock_path;
#if defined(OS_CHROMEOS)
  easy_unlock_path = base::FilePath("/usr/share/chromeos-assets/easy_unlock");
#endif  // defined(OS_CHROMEOS)

#ifndef NDEBUG
  // Only allow app path override switch for debug build.
  const CommandLine* command_line = CommandLine::ForCurrentProcess();
  if (command_line->HasSwitch(switches::kEasyUnlockAppPath)) {
    easy_unlock_path =
        command_line->GetSwitchValuePath(switches::kEasyUnlockAppPath);
  }
#endif  // !defined(NDEBUG)

  if (!easy_unlock_path.empty()) {
    extensions::ComponentLoader* loader = GetComponentLoader(profile_);
    if (!loader->Exists(extension_misc::kEasyUnlockAppId))
      loader->Add(IDR_EASY_UNLOCK_MANIFEST, easy_unlock_path);

    ExtensionService* extension_service =
        extensions::ExtensionSystem::Get(profile_)->extension_service();
    extension_service->EnableExtension(extension_misc::kEasyUnlockAppId);

    NotifyUserUpdated();
  }
#endif  // defined(GOOGLE_CHROME_BUILD)
}

void EasyUnlockService::DisableAppIfLoaded() {
  extensions::ComponentLoader* loader = GetComponentLoader(profile_);
  if (!loader->Exists(extension_misc::kEasyUnlockAppId))
    return;

  ExtensionService* extension_service =
      extensions::ExtensionSystem::Get(profile_)->extension_service();
  extension_service->DisableExtension(extension_misc::kEasyUnlockAppId,
                                      extensions::Extension::DISABLE_RELOAD);
}

void EasyUnlockService::UnloadApp() {
  GetComponentLoader(profile_)->Remove(extension_misc::kEasyUnlockAppId);
}

void EasyUnlockService::ReloadApp() {
  // Make sure lock screen state set by the extension gets reset.
  ResetScreenlockState();

  if (!GetComponentLoader(profile_)->Exists(extension_misc::kEasyUnlockAppId))
    return;
  extensions::ExtensionSystem* extension_system =
      extensions::ExtensionSystem::Get(profile_);
  extension_system->extension_service()->ReloadExtension(
      extension_misc::kEasyUnlockAppId);
  NotifyUserUpdated();
}

void EasyUnlockService::UpdateAppState() {
  if (IsAllowed()) {
    LoadApp();

#if defined(OS_CHROMEOS)
    if (!power_monitor_)
      power_monitor_.reset(new PowerMonitor(this));
#endif
  } else {
    bool bluetooth_waking_up = false;
#if defined(OS_CHROMEOS)
    // If the service is not allowed due to bluetooth not being detected just
    // after system suspend is done, give bluetooth more time to be detected
    // before disabling the app (and resetting screenlock state).
    bluetooth_waking_up =
        power_monitor_.get() && power_monitor_->waking_up() &&
        !bluetooth_detector_->IsPresent();
#endif

    if (!bluetooth_waking_up) {
      DisableAppIfLoaded();
      ResetScreenlockState();
#if defined(OS_CHROMEOS)
      power_monitor_.reset();
#endif
    }
  }
}

void EasyUnlockService::NotifyUserUpdated() {
  std::string user_id = GetUserEmail();
  if (user_id.empty())
    return;

  // Notify the easy unlock app that the user info changed.
  extensions::api::easy_unlock_private::UserInfo info;
  info.user_id = user_id;
  info.logged_in = GetType() == TYPE_REGULAR;
  info.data_ready = info.logged_in || GetRemoteDevices() != NULL;

  scoped_ptr<base::ListValue> args(new base::ListValue());
  args->Append(info.ToValue().release());

  scoped_ptr<extensions::Event> event(new extensions::Event(
      extensions::api::easy_unlock_private::OnUserInfoUpdated::kEventName,
      args.Pass()));

  extensions::EventRouter::Get(profile_)->DispatchEventToExtension(
       extension_misc::kEasyUnlockAppId, event.Pass());
}

void EasyUnlockService::NotifyTurnOffOperationStatusChanged() {
  FOR_EACH_OBSERVER(
      EasyUnlockServiceObserver, observers_, OnTurnOffOperationStatusChanged());
}

void EasyUnlockService::ResetScreenlockState() {
  screenlock_state_handler_.reset();
  auth_attempt_.reset();
}

void EasyUnlockService::SetScreenlockHardlockedState(
    EasyUnlockScreenlockStateHandler::HardlockState state) {
  if (screenlock_state_handler_)
    screenlock_state_handler_->SetHardlockState(state);
  if (state != EasyUnlockScreenlockStateHandler::NO_HARDLOCK)
    auth_attempt_.reset();
}

void EasyUnlockService::Initialize() {
  InitializeInternal();

#if defined(OS_CHROMEOS)
  // Only start Bluetooth detection for ChromeOS since the feature is
  // only offered on ChromeOS. Enabling this on non-ChromeOS platforms
  // previously introduced a performance regression: http://crbug.com/404482
  // Make sure not to reintroduce a performance regression if re-enabling on
  // additional platforms.
  // TODO(xiyuan): Revisit when non-chromeos platforms are supported.
  bluetooth_detector_->Initialize();
#endif  // defined(OS_CHROMEOS)
}

void EasyUnlockService::OnBluetoothAdapterPresentChanged() {
  UpdateAppState();
}

void EasyUnlockService::SetHardlockStateForUser(
      const std::string& user_id,
      EasyUnlockScreenlockStateHandler::HardlockState state) {
  DCHECK(!user_id.empty());

  PrefService* local_state = GetLocalState();
  if (!local_state)
    return;

  DictionaryPrefUpdate update(local_state, prefs::kEasyUnlockHardlockState);
  update->SetIntegerWithoutPathExpansion(user_id, static_cast<int>(state));

  if (GetUserEmail() == user_id)
    SetScreenlockHardlockedState(state);
}

#if defined(OS_CHROMEOS)
void EasyUnlockService::OnCryptohomeKeysFetchedForChecking(
    const std::string& user_id,
    const std::set<std::string> paired_devices,
    bool success,
    const chromeos::EasyUnlockDeviceKeyDataList& key_data_list) {
  DCHECK(!user_id.empty() && !paired_devices.empty());

  if (!success) {
    SetHardlockStateForUser(user_id,
                            EasyUnlockScreenlockStateHandler::NO_PAIRING);
    return;
  }

  std::set<std::string> devices_in_cryptohome;
  for (const auto& device_key_data : key_data_list)
    devices_in_cryptohome.insert(device_key_data.psk);

  if (paired_devices != devices_in_cryptohome ||
      GetHardlockState() == EasyUnlockScreenlockStateHandler::NO_PAIRING) {
    SetHardlockStateForUser(
        user_id,
        devices_in_cryptohome.empty()
            ? EasyUnlockScreenlockStateHandler::PAIRING_ADDED
            : EasyUnlockScreenlockStateHandler::PAIRING_CHANGED);
  }
}
#endif

void EasyUnlockService::PrepareForSuspend() {
  DisableAppIfLoaded();
  if (screenlock_state_handler_ && screenlock_state_handler_->IsActive()) {
    UpdateScreenlockState(
        EasyUnlockScreenlockStateHandler::STATE_BLUETOOTH_CONNECTING);
  }
}