chromium/ash/detachable_base/detachable_base_handler.cc

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

#include "ash/detachable_base/detachable_base_handler.h"

#include "ash/constants/ash_pref_names.h"
#include "ash/detachable_base/detachable_base_observer.h"
#include "ash/public/cpp/session/user_info.h"
#include "ash/shell.h"
#include "base/functional/bind.h"
#include "base/strings/string_number_conversions.h"
#include "base/values.h"
#include "components/account_id/account_id.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"

namespace ash {

namespace {

// Keys in local state used to persist detachable base pairing state.
// Example detachable base pref value:
// {
//   ash.detachable_base.devices: {
//     user_key_1: {
//       last_used: hex encoded base device ID
//     },
//     user_key_2: {
//        // no detachable_base device info, e.g. the user has not ever used it
//     }
//   }
// }
constexpr char kLastUsedByUserPrefKey[] = "last_used";

std::string GetKeyForPrefs(const AccountId& account_id) {
  if (account_id.HasAccountIdKey())
    return account_id.GetAccountIdKey();
  return account_id.GetUserEmail();
}

}  // namespace

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

DetachableBaseHandler::DetachableBaseHandler(PrefService* local_state)
    : local_state_(local_state),
      hammerd_observation_(this),
      power_manager_observation_(this) {
  if (HammerdClient::Get())  // May be null in tests
    hammerd_observation_.Observe(HammerdClient::Get());
  chromeos::PowerManagerClient* power_manager_client =
      chromeos::PowerManagerClient::Get();
  power_manager_observation_.Observe(power_manager_client);

  power_manager_client->GetSwitchStates(
      base::BindOnce(&DetachableBaseHandler::OnGotPowerManagerSwitchStates,
                     weak_ptr_factory_.GetWeakPtr()));

  if (GetPairingStatus() != DetachableBasePairingStatus::kNone)
    NotifyPairingStatusChanged();
}

DetachableBaseHandler::~DetachableBaseHandler() = default;

void DetachableBaseHandler::AddObserver(DetachableBaseObserver* observer) {
  observers_.AddObserver(observer);
}

void DetachableBaseHandler::RemoveObserver(DetachableBaseObserver* observer) {
  observers_.RemoveObserver(observer);
}

void DetachableBaseHandler::RemoveUserData(const UserInfo& user) {
  last_used_devices_.erase(user.account_id);

  if (local_state_) {
    ScopedDictPrefUpdate update(local_state_, prefs::kDetachableBaseDevices);
    update->Remove(GetKeyForPrefs(user.account_id));
  }
}

DetachableBasePairingStatus DetachableBaseHandler::GetPairingStatus() const {
  if (!tablet_mode_.has_value() ||
      *tablet_mode_ == chromeos::PowerManagerClient::TabletMode::ON) {
    return DetachableBasePairingStatus::kNone;
  }

  return pairing_status_;
}

bool DetachableBaseHandler::PairedBaseMatchesLastUsedByUser(
    const UserInfo& user) const {
  if (GetPairingStatus() != DetachableBasePairingStatus::kAuthenticated)
    return false;

  DCHECK(!authenticated_base_id_.empty());

  // If local state is not set for non-ephemeral users, the last used detachable
  // base cannot be determined at this point.
  // Assume no device has previously been used - the state will be evaluated
  // again when the local state prefs get initialzized.
  if (!local_state_ && !user.is_ephemeral)
    return true;

  DetachableBaseId last_used_base = GetLastUsedDeviceForUser(user);
  return last_used_base.empty() || authenticated_base_id_ == last_used_base;
}

bool DetachableBaseHandler::SetPairedBaseAsLastUsedByUser(
    const UserInfo& user) {
  if (GetPairingStatus() != DetachableBasePairingStatus::kAuthenticated)
    return false;

  // Do not update the last paired device for non-ephemeral users if local state
  // is not set, as the value would not be correctly preserved. Note that the
  // observers will be notified about the pairing status change when the local
  // state gets initialized - they should attempt to set this value at that
  // point.
  if (!local_state_ && !user.is_ephemeral)
    return false;

  last_used_devices_[user.account_id] = authenticated_base_id_;

  if (!user.is_ephemeral) {
    ScopedDictPrefUpdate update(local_state_, prefs::kDetachableBaseDevices);
    update->EnsureDict(GetKeyForPrefs(user.account_id))
        ->Set(kLastUsedByUserPrefKey, authenticated_base_id_);
  }

  return true;
}

void DetachableBaseHandler::BaseFirmwareUpdateNeeded() {
  NotifyBaseRequiresFirmwareUpdate(true /*requires_update*/);
}

void DetachableBaseHandler::BaseFirmwareUpdateStarted() {}

void DetachableBaseHandler::BaseFirmwareUpdateSucceeded() {
  NotifyBaseRequiresFirmwareUpdate(false /*requires_update*/);
}

void DetachableBaseHandler::BaseFirmwareUpdateFailed() {}

void DetachableBaseHandler::PairChallengeSucceeded(
    const std::vector<uint8_t>& base_id) {
  authenticated_base_id_ = base::HexEncode(base_id);
  pairing_status_ = DetachableBasePairingStatus::kAuthenticated;

  if (GetPairingStatus() != DetachableBasePairingStatus::kNone)
    NotifyPairingStatusChanged();
}

void DetachableBaseHandler::PairChallengeFailed() {
  authenticated_base_id_.clear();
  pairing_status_ = DetachableBasePairingStatus::kNotAuthenticated;

  if (GetPairingStatus() != DetachableBasePairingStatus::kNone)
    NotifyPairingStatusChanged();
}

void DetachableBaseHandler::InvalidBaseConnected() {
  authenticated_base_id_.clear();
  pairing_status_ = DetachableBasePairingStatus::kInvalidDevice;

  if (GetPairingStatus() != DetachableBasePairingStatus::kNone)
    NotifyPairingStatusChanged();
}

void DetachableBaseHandler::TabletModeEventReceived(
    chromeos::PowerManagerClient::TabletMode mode,
    base::TimeTicks timestamp) {
  UpdateTabletMode(mode);
}

void DetachableBaseHandler::OnGotPowerManagerSwitchStates(
    std::optional<chromeos::PowerManagerClient::SwitchStates> switch_states) {
  if (!switch_states.has_value() || tablet_mode_.has_value())
    return;

  UpdateTabletMode(switch_states->tablet_mode);
}

void DetachableBaseHandler::UpdateTabletMode(
    chromeos::PowerManagerClient::TabletMode mode) {
  DetachableBasePairingStatus old_pairing_status = GetPairingStatus();
  tablet_mode_ = mode;

  if (*tablet_mode_ == chromeos::PowerManagerClient::TabletMode::ON) {
    authenticated_base_id_.clear();
    pairing_status_ = DetachableBasePairingStatus::kNone;
    NotifyBaseRequiresFirmwareUpdate(false /*requires_update*/);
  }

  if (GetPairingStatus() != old_pairing_status)
    NotifyPairingStatusChanged();
}

DetachableBaseHandler::DetachableBaseId
DetachableBaseHandler::GetLastUsedDeviceForUser(const UserInfo& user) const {
  const auto it = last_used_devices_.find(user.account_id);
  // If the last used device was set within this session, bypass local state.
  if (it != last_used_devices_.end())
    return it->second;

  // For ephemeral lookup do not attempt getting the value from local state;
  // return empty string instead.
  if (user.is_ephemeral)
    return "";

  const base::Value::Dict& detachable_base_info =
      local_state_->GetDict(prefs::kDetachableBaseDevices);
  const base::Value::Dict* account_info =
      detachable_base_info.FindDictByDottedPath(
          GetKeyForPrefs(user.account_id));
  if (!account_info)
    return "";
  const std::string* last_used =
      account_info->FindString(kLastUsedByUserPrefKey);

  return last_used ? *last_used : "";
}

void DetachableBaseHandler::NotifyPairingStatusChanged() {
  for (auto& observer : observers_)
    observer.OnDetachableBasePairingStatusChanged(GetPairingStatus());
}

void DetachableBaseHandler::NotifyBaseRequiresFirmwareUpdate(
    bool requires_update) {
  for (auto& observer : observers_)
    observer.OnDetachableBaseRequiresUpdateChanged(requires_update);
}

}  // namespace ash