chromium/chromeos/ash/components/proximity_auth/proximity_monitor_impl.cc

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

#include "chromeos/ash/components/proximity_auth/proximity_monitor_impl.h"

#include <math.h>

#include <memory>
#include <utility>

#include "base/functional/bind.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "chromeos/ash/components/multidevice/logging/logging.h"
#include "chromeos/ash/components/proximity_auth/metrics.h"
#include "chromeos/ash/services/secure_channel/public/cpp/client/client_channel.h"
#include "device/bluetooth/bluetooth_adapter.h"
#include "device/bluetooth/bluetooth_adapter_factory.h"

namespace proximity_auth {

// The time to wait, in milliseconds, between proximity polling iterations.
const int kPollingTimeoutMs = 250;

// The weight of the most recent RSSI sample.
const double kRssiSampleWeight = 0.3;

// RSSI must be greater than this value to consider the host proximate, and
// allow unlock.
const int kRssiThreshold = -70;

ProximityMonitorImpl::ProximityMonitorImpl(
    ash::multidevice::RemoteDeviceRef remote_device,
    ash::secure_channel::ClientChannel* channel)
    : remote_device_(remote_device),
      channel_(channel),
      remote_device_is_in_proximity_(false),
      is_active_(false) {
  if (device::BluetoothAdapterFactory::IsBluetoothSupported()) {
    device::BluetoothAdapterFactory::Get()->GetAdapter(
        base::BindOnce(&ProximityMonitorImpl::OnAdapterInitialized,
                       weak_ptr_factory_.GetWeakPtr()));
  } else {
    PA_LOG(ERROR) << "[Proximity] Proximity monitoring unavailable: "
                  << "Bluetooth is unsupported on this platform.";
  }
}

ProximityMonitorImpl::~ProximityMonitorImpl() {}

void ProximityMonitorImpl::Start() {
  is_active_ = true;
  UpdatePollingState();
}

void ProximityMonitorImpl::Stop() {
  is_active_ = false;
  ClearProximityState();
  UpdatePollingState();
}

bool ProximityMonitorImpl::IsUnlockAllowed() const {
  return remote_device_is_in_proximity_;
}

void ProximityMonitorImpl::RecordProximityMetricsOnAuthSuccess() {
  double rssi_rolling_average = rssi_rolling_average_
                                    ? *rssi_rolling_average_
                                    : metrics::kUnknownProximityValue;

  std::string remote_device_model = metrics::kUnknownDeviceModel;
  ash::multidevice::RemoteDeviceRef remote_device = remote_device_;
  if (!remote_device.name().empty())
    remote_device_model = remote_device.name();

  metrics::RecordAuthProximityRollingRssi(round(rssi_rolling_average));
  metrics::RecordAuthProximityRemoteDeviceModelHash(remote_device_model);
}

void ProximityMonitorImpl::OnAdapterInitialized(
    scoped_refptr<device::BluetoothAdapter> adapter) {
  bluetooth_adapter_ = adapter;
  UpdatePollingState();
}

void ProximityMonitorImpl::UpdatePollingState() {
  if (ShouldPoll()) {
    // If there is a polling iteration already scheduled, wait for it.
    if (polling_weak_ptr_factory_.HasWeakPtrs())
      return;

    // Polling can re-entrantly call back into this method, so make sure to
    // schedule the next polling iteration prior to executing the current one.
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE,
        base::BindOnce(
            &ProximityMonitorImpl::PerformScheduledUpdatePollingState,
            polling_weak_ptr_factory_.GetWeakPtr()),
        base::Milliseconds(kPollingTimeoutMs));
    Poll();
  } else {
    polling_weak_ptr_factory_.InvalidateWeakPtrs();
    remote_device_is_in_proximity_ = false;
  }
}

void ProximityMonitorImpl::PerformScheduledUpdatePollingState() {
  polling_weak_ptr_factory_.InvalidateWeakPtrs();
  UpdatePollingState();
}

bool ProximityMonitorImpl::ShouldPoll() const {
  return is_active_ && bluetooth_adapter_;
}

void ProximityMonitorImpl::Poll() {
  DCHECK(ShouldPoll());

  if (channel_->is_disconnected()) {
    PA_LOG(ERROR) << "Channel is disconnected.";
    ClearProximityState();
    return;
  }

  channel_->GetConnectionMetadata(
      base::BindOnce(&ProximityMonitorImpl::OnGetConnectionMetadata,
                     weak_ptr_factory_.GetWeakPtr()));
}

void ProximityMonitorImpl::OnGetConnectionMetadata(
    ash::secure_channel::mojom::ConnectionMetadataPtr connection_metadata) {
  if (connection_metadata->bluetooth_connection_metadata)
    OnGetRssi(connection_metadata->bluetooth_connection_metadata->current_rssi);
  else
    OnGetRssi(std::nullopt);
}

void ProximityMonitorImpl::OnGetRssi(const std::optional<int32_t>& rssi) {
  if (!is_active_) {
    PA_LOG(VERBOSE) << "Received RSSI after stopping.";
    return;
  }

  if (rssi) {
    AddSample(*rssi);
  } else {
    PA_LOG(WARNING) << "Received invalid RSSI value.";
    rssi_rolling_average_.reset();
    CheckForProximityStateChange();
  }
}

void ProximityMonitorImpl::ClearProximityState() {
  if (is_active_ && remote_device_is_in_proximity_)
    NotifyProximityStateChanged();

  remote_device_is_in_proximity_ = false;
  rssi_rolling_average_.reset();
}

void ProximityMonitorImpl::AddSample(int32_t rssi) {
  double weight = kRssiSampleWeight;
  if (!rssi_rolling_average_) {
    rssi_rolling_average_ = std::make_unique<double>(rssi);
  } else {
    *rssi_rolling_average_ =
        weight * rssi + (1 - weight) * (*rssi_rolling_average_);
  }

  CheckForProximityStateChange();
}

void ProximityMonitorImpl::CheckForProximityStateChange() {
  bool is_now_in_proximity =
      rssi_rolling_average_ && *rssi_rolling_average_ > kRssiThreshold;

  if (rssi_rolling_average_ && !is_now_in_proximity) {
    PA_LOG(VERBOSE) << "Not in proximity. Rolling RSSI average: "
                    << *rssi_rolling_average_;
  }

  if (remote_device_is_in_proximity_ != is_now_in_proximity) {
    PA_LOG(VERBOSE) << "[Proximity] Updated proximity state: "
                    << (is_now_in_proximity ? "proximate" : "distant");
    remote_device_is_in_proximity_ = is_now_in_proximity;
    NotifyProximityStateChanged();
  }
}

}  // namespace proximity_auth