chromium/services/device/generic_sensor/platform_sensor_provider_chromeos.cc

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

#include "services/device/generic_sensor/platform_sensor_provider_chromeos.h"

#include <algorithm>
#include <iterator>
#include <map>
#include <utility>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/memory/scoped_refptr.h"
#include "base/not_fatal_until.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/sequenced_task_runner.h"
#include "chromeos/components/sensors/sensor_util.h"
#include "services/device/generic_sensor/platform_sensor_chromeos.h"

using chromeos::sensors::mojom::SensorDeviceDisconnectReason;

namespace device {
namespace {

constexpr base::TimeDelta kReconnectDelay = base::Milliseconds(1000);

std::optional<mojom::SensorType> ConvertSensorType(
    chromeos::sensors::mojom::DeviceType device_type) {
  switch (device_type) {
    case chromeos::sensors::mojom::DeviceType::ACCEL:
      return mojom::SensorType::ACCELEROMETER;

    case chromeos::sensors::mojom::DeviceType::ANGLVEL:
      return mojom::SensorType::GYROSCOPE;

    case chromeos::sensors::mojom::DeviceType::LIGHT:
      return mojom::SensorType::AMBIENT_LIGHT;

    case chromeos::sensors::mojom::DeviceType::MAGN:
      return mojom::SensorType::MAGNETOMETER;

    case chromeos::sensors::mojom::DeviceType::GRAVITY:
      return mojom::SensorType::GRAVITY;

    default:
      return std::nullopt;
  }
}

bool DeviceNeedsLocationWithTypes(const std::vector<mojom::SensorType>& types) {
  for (auto type : types) {
    switch (type) {
      case mojom::SensorType::AMBIENT_LIGHT:
      case mojom::SensorType::ACCELEROMETER:
      case mojom::SensorType::GYROSCOPE:
      case mojom::SensorType::MAGNETOMETER:
      case mojom::SensorType::GRAVITY:
        return true;
      default:
        break;
    }
  }

  return false;
}

}  // namespace

PlatformSensorProviderChromeOS::PlatformSensorProviderChromeOS() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  RegisterSensorClient();
}

PlatformSensorProviderChromeOS::~PlatformSensorProviderChromeOS() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

base::WeakPtr<PlatformSensorProvider>
PlatformSensorProviderChromeOS::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

void PlatformSensorProviderChromeOS::SetUpChannel(
    mojo::PendingRemote<chromeos::sensors::mojom::SensorService>
        pending_remote) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  if (sensor_service_remote_.is_bound()) {
    LOG(ERROR) << "Ignoring the second Remote<SensorService>";
    return;
  }

  sensor_service_remote_.Bind(std::move(pending_remote));
  sensor_service_remote_.set_disconnect_handler(
      base::BindOnce(&PlatformSensorProviderChromeOS::OnSensorServiceDisconnect,
                     weak_ptr_factory_.GetWeakPtr()));

  sensor_service_remote_->RegisterNewDevicesObserver(
      new_devices_observer_.BindNewPipeAndPassRemote());
  new_devices_observer_.set_disconnect_handler(base::BindOnce(
      &PlatformSensorProviderChromeOS::OnNewDevicesObserverDisconnect,
      weak_ptr_factory_.GetWeakPtr()));

  sensor_service_remote_->GetAllDeviceIds(
      base::BindOnce(&PlatformSensorProviderChromeOS::GetAllDeviceIdsCallback,
                     weak_ptr_factory_.GetWeakPtr()));
}

void PlatformSensorProviderChromeOS::OnNewDeviceAdded(
    int32_t iio_device_id,
    const std::vector<chromeos::sensors::mojom::DeviceType>& types) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (base::Contains(sensors_, iio_device_id))
    return;

  RegisterDevice(iio_device_id, types);
}

void PlatformSensorProviderChromeOS::CreateSensorInternal(
    mojom::SensorType type,
    CreateSensorCallback callback) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (!sensor_service_remote_.is_bound() || !AreAllSensorsReady()) {
    // Wait until |sensor_service_remote_| is connected and all sensors are
    // ready to proceed.
    // If |sensor_service_remote_| is disconnected, wait until it re-connects to
    // proceed, as it needs a valid SensorDevice channel.
    return;
  }

  // As mojom::SensorType::RELATIVE_ORIENTATION_EULER_ANGLES needs to know
  // whether mojom::SensorType::GYROSCOPE exists or not to determine the
  // algorithm, wait until all sensors ready before processing the fusion
  // sensors as well.
  if (IsFusionSensorType(type)) {
    CreateFusionSensor(type, std::move(callback));
    return;
  }

  auto id_opt = GetDeviceId(type);
  if (!id_opt.has_value()) {
    std::move(callback).Run(nullptr);
    return;
  }
  int32_t id = id_opt.value();
  DCHECK(base::Contains(sensors_, id));

  auto& sensor = sensors_[id];
  DCHECK(sensor.scale.has_value());

  auto sensor_device_remote = GetSensorDeviceRemote(id);
  std::move(callback).Run(base::MakeRefCounted<PlatformSensorChromeOS>(
      id, type, GetSensorReadingSharedBufferForType(type), AsWeakPtr(),
      base::BindOnce(&PlatformSensorProviderChromeOS::OnSensorDeviceDisconnect,
                     weak_ptr_factory_.GetWeakPtr(), id),
      sensor.scale.value(), std::move(sensor_device_remote)));
}

void PlatformSensorProviderChromeOS::FreeResources() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
}

bool PlatformSensorProviderChromeOS::IsFusionSensorType(
    mojom::SensorType type) const {
  // Let iioservice provide the Gravity sensor.
  switch (type) {
    case mojom::SensorType::LINEAR_ACCELERATION:
    case mojom::SensorType::ABSOLUTE_ORIENTATION_EULER_ANGLES:
    case mojom::SensorType::ABSOLUTE_ORIENTATION_QUATERNION:
    case mojom::SensorType::RELATIVE_ORIENTATION_EULER_ANGLES:
    case mojom::SensorType::RELATIVE_ORIENTATION_QUATERNION:
      return true;
    default:
      return false;
  }
}

bool PlatformSensorProviderChromeOS::IsSensorTypeAvailable(
    mojom::SensorType type) const {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  return GetDeviceId(type).has_value();
}

PlatformSensorProviderChromeOS::SensorData::SensorData() = default;
PlatformSensorProviderChromeOS::SensorData::~SensorData() = default;

std::optional<PlatformSensorProviderChromeOS::SensorLocation>
PlatformSensorProviderChromeOS::ParseLocation(
    const std::optional<std::string>& raw_location) {
  if (!raw_location.has_value()) {
    LOG(ERROR) << "No location attribute";
    return std::nullopt;
  }

  // These locations must be listed in the same order as the SensorLocation
  // enum.
  const std::vector<std::string> location_strings = {
      chromeos::sensors::mojom::kLocationBase,
      chromeos::sensors::mojom::kLocationLid,
      chromeos::sensors::mojom::kLocationCamera};
  const auto it = base::ranges::find(location_strings, raw_location.value());
  if (it == std::end(location_strings))
    return std::nullopt;

  return static_cast<SensorLocation>(
      std::distance(std::begin(location_strings), it));
}

std::optional<int32_t> PlatformSensorProviderChromeOS::GetDeviceId(
    mojom::SensorType type) const {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  const auto type_id = sensor_id_by_type_.find(type);
  if (type_id == sensor_id_by_type_.end())
    return std::nullopt;
  return type_id->second;
}

void PlatformSensorProviderChromeOS::RegisterSensorClient() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(!sensor_hal_client_.is_bound());

  if (!chromeos::sensors::BindSensorHalClient(
          sensor_hal_client_.BindNewPipeAndPassRemote())) {
    LOG(ERROR) << "Failed to bind SensorHalClient via Crosapi";
    return;
  }

  sensor_hal_client_.set_disconnect_handler(
      base::BindOnce(&PlatformSensorProviderChromeOS::OnSensorHalClientFailure,
                     weak_ptr_factory_.GetWeakPtr(), kReconnectDelay));
}

void PlatformSensorProviderChromeOS::OnSensorHalClientFailure(
    base::TimeDelta reconnection_delay) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(ERROR) << "OnSensorHalClientFailure";

  ResetSensorService();
  sensor_hal_client_.reset();

  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE,
      base::BindOnce(&PlatformSensorProviderChromeOS::RegisterSensorClient,
                     weak_ptr_factory_.GetWeakPtr()),
      reconnection_delay);
}

void PlatformSensorProviderChromeOS::OnSensorServiceDisconnect() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(ERROR) << "OnSensorServiceDisconnect";

  ResetSensorService();
}

void PlatformSensorProviderChromeOS::OnNewDevicesObserverDisconnect() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  LOG(ERROR) << "OnNewDevicesObserverDisconnect";

  // Assumes IIO Service has crashed and waits for its relaunch.
  ResetSensorService();
}

void PlatformSensorProviderChromeOS::ResetSensorService() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  for (auto& sensor : sensors_) {
    // Reset only the remote while keeping the attributes information to speed
    // up recovery, as the attributes won't need to be queried again.
    sensor.second.remote.reset();
  }

  // Reset the existing PlatformSensors as well.
  for (const auto& type_id : sensor_id_by_type_)
    ReplaceAndRemoveSensor(type_id.first);

  new_devices_observer_.reset();
  sensor_service_remote_.reset();
}

void PlatformSensorProviderChromeOS::GetAllDeviceIdsCallback(
    const SensorIdTypesMap& ids_types) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(sensor_service_remote_.is_bound());

  sensor_ids_received_ = true;

  for (const auto& id_types : ids_types)
    RegisterDevice(id_types.first, id_types.second);

  ProcessSensorsIfPossible();
}

void PlatformSensorProviderChromeOS::RegisterDevice(
    int32_t id,
    const std::vector<chromeos::sensors::mojom::DeviceType>& types) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  SensorData& sensor = sensors_[id];

  if (sensor.ignored)
    return;

  for (const auto& device_type : types) {
    auto type_opt = ConvertSensorType(device_type);
    if (!type_opt.has_value())
      continue;

    sensor.types.push_back(type_opt.value());
  }

  if (sensor.types.empty()) {
    sensor.ignored = true;
    return;
  }

  sensor.remote.reset();

  std::vector<std::string> attr_names;
  if (!sensor.scale.has_value())
    attr_names.push_back(chromeos::sensors::mojom::kScale);
  if (DeviceNeedsLocationWithTypes(sensor.types) &&
      !sensor.location.has_value()) {
    attr_names.push_back(chromeos::sensors::mojom::kLocation);
  }

  if (attr_names.empty())
    return;

  sensor.remote = GetSensorDeviceRemote(id);

  // Add a temporary disconnect handler to catch failures during sensor
  // enumeration. PlatformSensorChromeOS will handle disconnection during
  // normal operation.
  sensor.remote.set_disconnect_with_reason_handler(
      base::BindOnce(&PlatformSensorProviderChromeOS::OnSensorDeviceDisconnect,
                     weak_ptr_factory_.GetWeakPtr(), id));

  sensor.remote->GetAttributes(
      std::move(attr_names),
      base::BindOnce(&PlatformSensorProviderChromeOS::GetAttributesCallback,
                     weak_ptr_factory_.GetWeakPtr(), id));
}

void PlatformSensorProviderChromeOS::GetAttributesCallback(
    int32_t id,
    const std::vector<std::optional<std::string>>& values) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  auto it = sensors_.find(id);
  CHECK(it != sensors_.end(), base::NotFatalUntil::M130);
  auto& sensor = it->second;
  DCHECK(sensor.remote.is_bound());

  size_t index = 0;

  if (!sensor.scale.has_value()) {
    if (index >= values.size()) {
      LOG(ERROR) << "values doesn't contain scale attribute.";
      IgnoreSensor(sensor);
      return;
    }

    double scale = 0.0;
    if (!values[index].has_value() ||
        !base::StringToDouble(values[index].value(), &scale)) {
      LOG(ERROR) << "Invalid scale: " << values[index].value_or("")
                 << ", for accel with id: " << id;
      IgnoreSensor(sensor);
      return;
    }

    sensor.scale = scale;

    ++index;
  }

  if (DeviceNeedsLocationWithTypes(sensor.types) &&
      !sensor.location.has_value()) {
    if (index >= values.size()) {
      LOG(ERROR) << "values doesn't contain location attribute.";
      IgnoreSensor(sensor);
      return;
    }

    sensor.location = ParseLocation(values[index]);
    if (!sensor.location.has_value()) {
      LOG(ERROR) << "Failed to parse location: " << values[index].value_or("")
                 << ", with sensor id: " << id;
      IgnoreSensor(sensor);
      return;
    }

    ++index;
  }

  ProcessSensorsIfPossible();
}

void PlatformSensorProviderChromeOS::IgnoreSensor(SensorData& sensor) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  sensor.ignored = true;
  sensor.remote.reset();

  ProcessSensorsIfPossible();
}

bool PlatformSensorProviderChromeOS::AreAllSensorsReady() const {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  if (!sensor_ids_received_)
    return false;

  return base::ranges::all_of(sensors_, [](const auto& sensor) {
    return sensor.second.ignored ||
           (sensor.second.scale.has_value() &&
            (!DeviceNeedsLocationWithTypes(sensor.second.types) ||
             sensor.second.location.has_value()));
  });
}

void PlatformSensorProviderChromeOS::OnSensorDeviceDisconnect(
    int32_t id,
    uint32_t custom_reason_code,
    const std::string& description) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  auto reason =
      static_cast<chromeos::sensors::mojom::SensorDeviceDisconnectReason>(
          custom_reason_code);
  LOG(ERROR) << "OnSensorDeviceDisconnect: " << id << ", reason: " << reason
             << ", description: " << description;

  switch (reason) {
    case SensorDeviceDisconnectReason::IIOSERVICE_CRASHED:
      ResetSensorService();
      break;

    case SensorDeviceDisconnectReason::DEVICE_REMOVED:
      // Hot-pluggable sensors should be HID-stack sensors and shouldn't have
      // the location attribute.
      if (sensors_[id].location.has_value()) {
        LOG(WARNING) << "Device being removed has location: "
                     << static_cast<int>(sensors_[id].location.value());
      }

      std::erase_if(sensor_id_by_type_, [this, &id](const auto& entry) {
        if (entry.second == id) {
          ReplaceAndRemoveSensor(entry.first);
          return true;
        }
        return false;
      });

      sensors_.erase(id);
      ProcessSensorsIfPossible();
      break;
  }
}

void PlatformSensorProviderChromeOS::ReplaceAndRemoveSensor(
    mojom::SensorType type) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  auto* platform_sensor = GetSensor(type).get();
  if (platform_sensor)
    platform_sensor->SensorReplaced();

  RemoveSensor(type, platform_sensor);
}

void PlatformSensorProviderChromeOS::ProcessSensorsIfPossible() {
  if (!AreAllSensorsReady())
    return;

  DetermineMotionSensors();
  DetermineLightSensor();

  RemoveUnusedSensorDeviceRemotes();
  ProcessStoredRequests();
}

// Follow Android's and W3C's requirements of motion sensors:
// Android: https://source.android.com/devices/sensors/sensor-types
// W3C: https://w3c.github.io/sensors/#local-coordinate-system
// To implement fusion/composite sensors, accelerometer, gyroscope (and
// magnetometer) must be in the same plane, so when it comes to choosing an
// accelerometer in a convertible device, we choose the one which is in the
// same place as the gyroscope. If we still have a choice, we use the one in
// the lid.
void PlatformSensorProviderChromeOS::DetermineMotionSensors() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  struct MotionSensorInfo {
    size_t count;
    std::vector<std::pair<int32_t, mojom::SensorType>> sensor_info_pairs;
  };
  std::map<SensorLocation, MotionSensorInfo> motion_sensor_location_info;

  for (const auto& sensor : sensors_) {
    if (sensor.second.ignored || !sensor.second.location)
      continue;

    SensorLocation location = sensor.second.location.value();
    DCHECK_LT(location, SensorLocation::kMax);

    for (auto type : sensor.second.types) {
      switch (type) {
        case mojom::SensorType::ACCELEROMETER:
        case mojom::SensorType::GYROSCOPE:
        case mojom::SensorType::MAGNETOMETER: {
          auto& motion_sensor_info = motion_sensor_location_info[location];
          motion_sensor_info.count++;
          motion_sensor_info.sensor_info_pairs.push_back(
              std::make_pair(sensor.first, type));
          break;
        }

        case mojom::SensorType::GRAVITY: {
          auto& motion_sensor_info = motion_sensor_location_info[location];
          // Don't need to increase |motion_sensor_info.count|, as gravity
          // sensors shouldn't influence the decision.
          motion_sensor_info.sensor_info_pairs.push_back(
              std::make_pair(sensor.first, type));
          break;
        }

        default:
          break;
      }
    }
  }

  const auto preferred_location =
      motion_sensor_location_info[SensorLocation::kLid].count >=
              motion_sensor_location_info[SensorLocation::kBase].count
          ? SensorLocation::kLid
          : SensorLocation::kBase;
  const auto& sensor_info_pairs =
      motion_sensor_location_info[preferred_location].sensor_info_pairs;

  for (const auto& pair : sensor_info_pairs)
    UpdateSensorIdMapping(pair.second, pair.first);
}

// Prefer the light sensor on the lid, as it's more meaningful to web API users.
void PlatformSensorProviderChromeOS::DetermineLightSensor() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  std::optional<int32_t> id = std::nullopt;

  for (const auto& sensor : sensors_) {
    if (sensor.second.ignored ||
        !base::Contains(sensor.second.types, mojom::SensorType::AMBIENT_LIGHT))
      continue;

    if (!id.has_value() || sensor.second.location == SensorLocation::kLid)
      id = sensor.first;
  }

  if (id.has_value())
    UpdateSensorIdMapping(mojom::SensorType::AMBIENT_LIGHT, id.value());
}

void PlatformSensorProviderChromeOS::UpdateSensorIdMapping(
    const mojom::SensorType& type,
    int32_t id) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  auto it = sensor_id_by_type_.find(type);
  if (it != sensor_id_by_type_.end() && it->second != id)
    ReplaceAndRemoveSensor(type);

  sensor_id_by_type_[type] = id;
}

void PlatformSensorProviderChromeOS::RemoveUnusedSensorDeviceRemotes() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  std::set<int32_t> used_ids;
  for (const auto& type_id : sensor_id_by_type_)
    used_ids.emplace(type_id.second);

  for (auto& sensor : sensors_) {
    if (!base::Contains(used_ids, sensor.first))
      sensor.second.remote.reset();
  }
}

void PlatformSensorProviderChromeOS::ProcessStoredRequests() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  std::vector<mojom::SensorType> request_types = GetPendingRequestTypes();
  for (const auto& type : request_types) {
    if (IsFusionSensorType(type)) {
      CreateFusionSensor(
          type,
          base::BindOnce(&PlatformSensorProviderChromeOS::NotifySensorCreated,
                         weak_ptr_factory_.GetWeakPtr(), type));
      continue;
    }

    SensorReadingSharedBuffer* reading_buffer =
        GetSensorReadingSharedBufferForType(type);

    if (!reading_buffer) {
      continue;
    }

    auto id_opt = GetDeviceId(type);
    if (!id_opt.has_value()) {
      NotifySensorCreated(type, nullptr);
      continue;
    }

    int32_t id = id_opt.value();
    DCHECK(base::Contains(sensors_, id));

    auto& sensor = sensors_[id];
    DCHECK(sensor.scale.has_value());

    auto sensor_device_remote = GetSensorDeviceRemote(id);
    NotifySensorCreated(
        type, base::MakeRefCounted<PlatformSensorChromeOS>(
                  id, type, reading_buffer, AsWeakPtr(),
                  base::BindOnce(
                      &PlatformSensorProviderChromeOS::OnSensorDeviceDisconnect,
                      weak_ptr_factory_.GetWeakPtr(), id),
                  sensor.scale.value(), std::move(sensor_device_remote)));
  }
}

mojo::Remote<chromeos::sensors::mojom::SensorDevice>
PlatformSensorProviderChromeOS::GetSensorDeviceRemote(int32_t id) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  DCHECK(sensor_service_remote_.is_bound());

  mojo::Remote<chromeos::sensors::mojom::SensorDevice> sensor_device_remote;
  auto& sensor = sensors_[id];
  if (sensor.remote.is_bound()) {
    // Reuse the previous remote.
    sensor_device_remote = std::move(sensor.remote);
  } else {
    sensor_service_remote_->GetDevice(
        id, sensor_device_remote.BindNewPipeAndPassReceiver());
  }

  return sensor_device_remote;
}

}  // namespace device