chromium/services/device/geolocation/win/location_provider_winrt.cc

// Copyright 2019 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/geolocation/win/location_provider_winrt.h"

#include <windows.devices.enumeration.h>
#include <windows.foundation.h>
#include <wrl/event.h>

#include <optional>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/win/core_winrt_util.h"
#include "components/device_event_log/device_event_log.h"
#include "services/device/public/cpp/device_features.h"
#include "services/device/public/cpp/geolocation/geoposition.h"
#include "services/device/public/mojom/geolocation_internals.mojom-shared.h"

namespace device {

namespace {
using ABI::Windows::Devices::Enumeration::DeviceAccessStatus;
using ABI::Windows::Devices::Enumeration::DeviceClass;
using ABI::Windows::Devices::Enumeration::IDeviceAccessInformation;
using ABI::Windows::Devices::Enumeration::IDeviceAccessInformationStatics;
using ABI::Windows::Devices::Geolocation::AltitudeReferenceSystem;
using ABI::Windows::Devices::Geolocation::AltitudeReferenceSystem_Ellipsoid;
using ABI::Windows::Devices::Geolocation::AltitudeReferenceSystem_Unspecified;
using ABI::Windows::Devices::Geolocation::BasicGeoposition;
using ABI::Windows::Devices::Geolocation::Geolocator;
using ABI::Windows::Devices::Geolocation::IGeocoordinate;
using ABI::Windows::Devices::Geolocation::IGeocoordinateWithPoint;
using ABI::Windows::Devices::Geolocation::IGeolocator;
using ABI::Windows::Devices::Geolocation::IGeopoint;
using ABI::Windows::Devices::Geolocation::IGeoposition;
using ABI::Windows::Devices::Geolocation::IGeoshape;
using ABI::Windows::Devices::Geolocation::IPositionChangedEventArgs;
using ABI::Windows::Devices::Geolocation::IStatusChangedEventArgs;
using ABI::Windows::Devices::Geolocation::PositionAccuracy;
using ABI::Windows::Devices::Geolocation::PositionChangedEventArgs;
using ABI::Windows::Devices::Geolocation::PositionStatus;
using ABI::Windows::Devices::Geolocation::StatusChangedEventArgs;
using ABI::Windows::Foundation::IReference;
using ABI::Windows::Foundation::ITypedEventHandler;
using ABI::Windows::Foundation::TimeSpan;
using Microsoft::WRL::ComPtr;

// The amount of change (in meters) in the reported position from the Windows
// API which will trigger an update.
constexpr double kDefaultMovementThresholdMeters = 1.0;

template <typename F>
std::optional<DOUBLE> GetOptionalDouble(F&& getter) {
  DOUBLE value = 0;
  HRESULT hr = getter(&value);
  if (SUCCEEDED(hr))
    return value;
  return std::nullopt;
}

template <typename F>
std::optional<DOUBLE> GetReferenceOptionalDouble(F&& getter) {
  IReference<DOUBLE>* reference_value;
  HRESULT hr = getter(&reference_value);
  if (!SUCCEEDED(hr) || !reference_value)
    return std::nullopt;
  return GetOptionalDouble([&](DOUBLE* value) -> HRESULT {
    return reference_value->get_Value(value);
  });
}

bool IsSystemLocationSettingEnabled() {
  ComPtr<IDeviceAccessInformationStatics> dev_access_info_statics;
  HRESULT hr = base::win::GetActivationFactory<
      IDeviceAccessInformationStatics,
      RuntimeClass_Windows_Devices_Enumeration_DeviceAccessInformation>(
      &dev_access_info_statics);
  if (FAILED(hr)) {
    GEOLOCATION_LOG(ERROR) << "IDeviceAccessInformationStatics failed: "
                           << logging::SystemErrorCodeToString(hr);
    return true;
  }

  ComPtr<IDeviceAccessInformation> dev_access_info;
  hr = dev_access_info_statics->CreateFromDeviceClass(
      DeviceClass::DeviceClass_Location, &dev_access_info);
  if (FAILED(hr)) {
    GEOLOCATION_LOG(ERROR) << "IDeviceAccessInformation failed: "
                           << logging::SystemErrorCodeToString(hr);
    return true;
  }

  auto status = DeviceAccessStatus::DeviceAccessStatus_Unspecified;
  dev_access_info->get_CurrentStatus(&status);

  return !(status == DeviceAccessStatus::DeviceAccessStatus_DeniedBySystem ||
           status == DeviceAccessStatus::DeviceAccessStatus_DeniedByUser);
}

ComPtr<IGeopoint> GetPointFromCoordinate(
    const ComPtr<IGeocoordinate>& coordinate) {
  ComPtr<IGeocoordinateWithPoint> coordinate_with_point = nullptr;
  const HRESULT query_result = coordinate.As(&coordinate_with_point);
  if (FAILED(query_result) || !coordinate_with_point) {
    GEOLOCATION_LOG(ERROR) << "Failed to cast to GeocoordinateWithPoint. "
                           << logging::SystemErrorCodeToString(query_result);
    return nullptr;
  }

  ComPtr<IGeopoint> point = nullptr;
  const HRESULT point_result = coordinate_with_point->get_Point(&point);
  if (FAILED(point_result)) {
    GEOLOCATION_LOG(ERROR) << "Failed to get point from coordinate. "
                           << logging::SystemErrorCodeToString(point_result);
    return nullptr;
  }
  return point;
}

AltitudeReferenceSystem GetAltitudeReferenceSystemFromPoint(
    const ComPtr<IGeopoint>& point) {
  ComPtr<IGeoshape> shape;
  HRESULT hr = point.As(&shape);
  if (FAILED(hr)) {
    GEOLOCATION_LOG(ERROR) << "Failed to cast to GeoShape. "
                           << logging::SystemErrorCodeToString(hr);
    return AltitudeReferenceSystem_Unspecified;
  }

  AltitudeReferenceSystem reference_system =
      AltitudeReferenceSystem_Unspecified;
  hr = shape->get_AltitudeReferenceSystem(&reference_system);
  if (FAILED(hr)) {
    GEOLOCATION_LOG(ERROR) << "Failed to get altitude reference system. "
                           << logging::SystemErrorCodeToString(hr);
    return AltitudeReferenceSystem_Unspecified;
  }
  return reference_system;
}

void RecordUmaStartProviderError(HRESULT result) {
  base::UmaHistogramSparse("Geolocation.LocationProviderWinrt.StartProvider",
                           result);
}

void RecordUmaRegisterCallbacksError(HRESULT result) {
  base::UmaHistogramSparse(
      "Geolocation.LocationProviderWinrt.RegisterCallbacks", result);
}

void RecordUmaOnPositionChangedError(HRESULT result) {
  base::UmaHistogramSparse(
      "Geolocation.LocationProviderWinrt.OnPositionChanged", result);
}

void RecordUmaOnStatusChangedError(HRESULT result) {
  base::UmaHistogramSparse("Geolocation.LocationProviderWinrt.OnStatusChanged",
                           result);
}

void RecordUmaErrorStatus(
    ABI::Windows::Devices::Geolocation::PositionStatus status) {
  base::UmaHistogramSparse("Geolocation.LocationProviderWinrt.ErrorStatus",
                           static_cast<int>(status));
}

void RecordUmaCreateGeopositionError(HRESULT result) {
  base::UmaHistogramSparse(
      "Geolocation.LocationProviderWinrt.CreateGeoposition", result);
}

}  // namespace

// LocationProviderWinrt
LocationProviderWinrt::LocationProviderWinrt() = default;

LocationProviderWinrt::~LocationProviderWinrt() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  StopProvider();
}

void LocationProviderWinrt::FillDiagnostics(
    mojom::GeolocationDiagnostics& diagnostics) {
  if (!is_started_) {
    diagnostics.provider_state =
        mojom::GeolocationDiagnostics::ProviderState::kStopped;
  } else if (!permission_granted_) {
    diagnostics.provider_state = mojom::GeolocationDiagnostics::ProviderState::
        kBlockedBySystemPermission;
  } else if (enable_high_accuracy_) {
    diagnostics.provider_state =
        mojom::GeolocationDiagnostics::ProviderState::kHighAccuracy;
  } else {
    diagnostics.provider_state =
        mojom::GeolocationDiagnostics::ProviderState::kLowAccuracy;
  }
}

void LocationProviderWinrt::SetUpdateCallback(
    const LocationProviderUpdateCallback& callback) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  location_update_callback_ = callback;
}

void LocationProviderWinrt::StartProvider(bool high_accuracy) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  is_started_ = true;
  enable_high_accuracy_ = high_accuracy;

  HRESULT hr = S_OK;

  if (!geo_locator_) {
    hr = GetGeolocator(&geo_locator_);
    if (FAILED(hr)) {
      RecordUmaStartProviderError(hr);
      HandleErrorCondition(mojom::GeopositionErrorCode::kPositionUnavailable,
                           "Unable to create instance of Geolocation API. " +
                               logging::SystemErrorCodeToString(hr));
      return;
    }

    hr = geo_locator_->put_MovementThreshold(kDefaultMovementThresholdMeters);

    if (FAILED(hr)) {
      RecordUmaStartProviderError(hr);
      GEOLOCATION_LOG(ERROR)
          << "Failed to set movement threshold on Geolocator. "
          << logging::SystemErrorCodeToString(hr);
    }
  }

  hr = geo_locator_->put_DesiredAccuracy(
      enable_high_accuracy_ ? PositionAccuracy::PositionAccuracy_High
                            : PositionAccuracy::PositionAccuracy_Default);

  if (FAILED(hr)) {
    RecordUmaStartProviderError(hr);
    GEOLOCATION_LOG(ERROR) << "Failed to set DesiredAccuracy on Geolocator: "
                           << logging::SystemErrorCodeToString(hr);
  }

  RegisterCallbacks();
}

void LocationProviderWinrt::StopProvider() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);

  is_started_ = false;

  // Reset the reference location state (provider+position)
  // so that future starts use fresh locations from
  // the newly constructed providers.
  last_result_.reset();
  if (!geo_locator_) {
    return;
  }

  UnregisterCallbacks();

  geo_locator_.Reset();
}

const mojom::GeopositionResult* LocationProviderWinrt::GetPosition() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  return last_result_.get();
}

void LocationProviderWinrt::OnPermissionGranted() {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  const bool was_permission_granted = permission_granted_;
  permission_granted_ = true;
  if (!was_permission_granted) {
    RegisterCallbacks();
  }
}

void LocationProviderWinrt::HandleErrorCondition(
    mojom::GeopositionErrorCode position_error_code,
    const std::string& position_error_message) {
  GEOLOCATION_LOG(ERROR) << position_error_message;
  last_result_ =
      mojom::GeopositionResult::NewError(mojom::GeopositionError::New(
          position_error_code, position_error_message, /*error_technical=*/""));
  if (location_update_callback_) {
    location_update_callback_.Run(this, last_result_.Clone());
  }
}

bool LocationProviderWinrt::HasValidLastPosition() const {
  return last_result_ && last_result_->is_position() &&
         ValidateGeoposition(*last_result_->get_position());
}

void LocationProviderWinrt::RegisterCallbacks() {
  if (!permission_granted_ || !geo_locator_) {
    return;
  }

  HRESULT hr = S_OK;

  if (!position_changed_token_) {
    position_received_ = false;
    EventRegistrationToken tmp_position_token;
    hr = geo_locator_->add_PositionChanged(
        Microsoft::WRL::Callback<
            ITypedEventHandler<Geolocator*, PositionChangedEventArgs*>>(
            [task_runner(base::SingleThreadTaskRunner::GetCurrentDefault()),
             callback(
                 base::BindRepeating(&LocationProviderWinrt::OnPositionChanged,
                                     weak_ptr_factory_.GetWeakPtr()))](
                IGeolocator* sender, IPositionChangedEventArgs* args) {
              task_runner->PostTask(
                  FROM_HERE,
                  base::BindOnce(callback, ComPtr<IGeolocator>(sender),
                                 ComPtr<IPositionChangedEventArgs>(args)));
              return S_OK;
            })
            .Get(),
        &tmp_position_token);

    if (FAILED(hr)) {
      RecordUmaRegisterCallbacksError(hr);
      if (!HasValidLastPosition()) {
        HandleErrorCondition(
            mojom::GeopositionErrorCode::kPositionUnavailable,
            "Unable to add a callback to retrieve position for Geolocation "
            "API. " +
                logging::SystemErrorCodeToString(hr));
      }
      return;
    }

    position_callback_initialized_time_ = base::TimeTicks::Now();
    position_changed_token_ = tmp_position_token;
  }

  if (!status_changed_token_) {
    EventRegistrationToken tmp_status_token;
    hr = geo_locator_->add_StatusChanged(
        Microsoft::WRL::Callback<
            ITypedEventHandler<Geolocator*, StatusChangedEventArgs*>>(
            [task_runner(base::SingleThreadTaskRunner::GetCurrentDefault()),
             callback(
                 base::BindRepeating(&LocationProviderWinrt::OnStatusChanged,
                                     weak_ptr_factory_.GetWeakPtr()))](
                IGeolocator* sender, IStatusChangedEventArgs* args) {
              task_runner->PostTask(
                  FROM_HERE,
                  base::BindOnce(callback, ComPtr<IGeolocator>(sender),
                                 ComPtr<IStatusChangedEventArgs>(args)));
              return S_OK;
            })
            .Get(),
        &tmp_status_token);

    if (FAILED(hr)) {
      RecordUmaRegisterCallbacksError(hr);
      // If this occurs we may still be able to provide a position update, but
      // if the geoloc API is Disabled(denied permission) we won't inform the
      // user, it will just fail silently or timeout.
      GEOLOCATION_LOG(ERROR)
          << "Failed to set a Status Changed callback for geolocation API. "
          << logging::SystemErrorCodeToString(hr);
      return;
    }

    status_changed_token_ = tmp_status_token;
  }
}

void LocationProviderWinrt::UnregisterCallbacks() {
  if (!geo_locator_) {
    return;
  }

  if (position_changed_token_) {
    geo_locator_->remove_PositionChanged(*position_changed_token_);
    position_changed_token_.reset();
  }

  if (status_changed_token_) {
    geo_locator_->remove_StatusChanged(*status_changed_token_);
    status_changed_token_.reset();
  }

  weak_ptr_factory_.InvalidateWeakPtrs();
}

void LocationProviderWinrt::OnPositionChanged(
    IGeolocator* geo_locator,
    IPositionChangedEventArgs* position_update) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  ComPtr<IGeoposition> position;
  HRESULT hr = position_update->get_Position(&position);
  if (FAILED(hr)) {
    RecordUmaOnPositionChangedError(hr);
    if (!HasValidLastPosition()) {
      HandleErrorCondition(mojom::GeopositionErrorCode::kPositionUnavailable,
                           "Unable to get position from Geolocation API. " +
                               logging::SystemErrorCodeToString(hr));
    }
    return;
  }

  mojom::GeopositionPtr location_data = CreateGeoposition(position.Get());
  if (!location_data || !ValidateGeoposition(*location_data)) {
    return;
  }

  last_result_ =
      mojom::GeopositionResult::NewPosition(std::move(location_data));

  if (!position_received_) {
    const base::TimeDelta time_to_first_position =
        base::TimeTicks::Now() - position_callback_initialized_time_;

    UmaHistogramCustomTimes(
        "Geolocation.LocationProviderWinrt.TimeToFirstPosition",
        time_to_first_position, base::Milliseconds(1), base::Seconds(10), 100);
    position_received_ = true;
  }

  if (location_update_callback_) {
    location_update_callback_.Run(this, last_result_.Clone());
  }
}

void LocationProviderWinrt::OnStatusChanged(
    IGeolocator* geo_locator,
    IStatusChangedEventArgs* status_update) {
  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
  PositionStatus status;
  HRESULT hr = status_update->get_Status(&status);
  if (FAILED(hr)) {
    RecordUmaOnStatusChangedError(hr);
    GEOLOCATION_LOG(ERROR)
        << "Failed to get a status from StatusChangedEventArgs. "
        << logging::SystemErrorCodeToString(hr);
    return;
  }

  position_status_ = status;

  switch (status) {
    case PositionStatus::PositionStatus_Disabled:
      RecordUmaErrorStatus(status);
      HandleErrorCondition(mojom::GeopositionErrorCode::kPermissionDenied,
                           "User has not allowed access to Windows Location.");
      break;
    case PositionStatus::PositionStatus_NotAvailable:
      RecordUmaErrorStatus(status);
      HandleErrorCondition(
          mojom::GeopositionErrorCode::kPositionUnavailable,
          "Location API is not available on this version of Windows.");
      break;
    default:
      break;
  }
}

mojom::GeopositionPtr LocationProviderWinrt::CreateGeoposition(
    IGeoposition* geoposition) {
  ComPtr<IGeocoordinate> coordinate;
  HRESULT hr = geoposition->get_Coordinate(&coordinate);
  if (FAILED(hr)) {
    RecordUmaCreateGeopositionError(hr);
    GEOLOCATION_LOG(ERROR)
        << "Failed to get a coordinate from geoposition from windows "
           "geolocation API. "
        << logging::SystemErrorCodeToString(hr);
    return nullptr;
  }

  ComPtr<IGeopoint> point = GetPointFromCoordinate(coordinate);
  if (!point) {
    RecordUmaCreateGeopositionError(E_POINTER);
    return nullptr;
  }

  BasicGeoposition position;
  hr = point->get_Position(&position);
  if (FAILED(hr)) {
    GEOLOCATION_LOG(ERROR) << "Failed to get position from point. "
                           << logging::SystemErrorCodeToString(hr);
    RecordUmaCreateGeopositionError(hr);
    return nullptr;
  }

  auto location_data = mojom::Geoposition::New();
  location_data->latitude = position.Latitude;
  location_data->longitude = position.Longitude;
  location_data->altitude = position.Altitude;
  location_data->accuracy = GetOptionalDouble([&](DOUBLE* value) -> HRESULT {
                              return coordinate->get_Accuracy(value);
                            }).value_or(device::mojom::kBadAccuracy);
  location_data->altitude_accuracy =
      GetReferenceOptionalDouble([&](IReference<DOUBLE>** value) -> HRESULT {
        return coordinate->get_AltitudeAccuracy(value);
      }).value_or(device::mojom::kBadAccuracy);
  location_data->heading =
      GetReferenceOptionalDouble([&](IReference<DOUBLE>** value) -> HRESULT {
        return coordinate->get_Heading(value);
      }).value_or(device::mojom::kBadHeading);
  location_data->speed =
      GetReferenceOptionalDouble([&](IReference<DOUBLE>** value) -> HRESULT {
        return coordinate->get_Speed(value);
      }).value_or(device::mojom::kBadSpeed);
  location_data->timestamp = base::Time::Now();

  // Overwrite the altitude if the accuracy is known to be bad or it is not
  // using ellipsoid altitude reference system.
  if (location_data->altitude_accuracy == device::mojom::kBadAccuracy ||
      GetAltitudeReferenceSystemFromPoint(point) !=
          AltitudeReferenceSystem_Ellipsoid) {
    location_data->altitude = device::mojom::kBadAltitude;
  }

  return location_data;
}

HRESULT LocationProviderWinrt::GetGeolocator(IGeolocator** geo_locator) {
  ComPtr<IGeolocator> temp_geo_locator;
  auto hstring = base::win::ScopedHString::Create(
      RuntimeClass_Windows_Devices_Geolocation_Geolocator);
  HRESULT hr = base::win::RoActivateInstance(hstring.get(), &temp_geo_locator);

  if (SUCCEEDED(hr)) {
    *geo_locator = temp_geo_locator.Detach();
  }

  return hr;
}

std::unique_ptr<LocationProvider> NewSystemLocationProvider() {
  // TODO: Remove IsSystemLocationSettingEnabled once
  // system permission manager support for `LocationProviderWinrt` is ready.
  if (!IsSystemLocationSettingEnabled()) {
    return nullptr;
  }

  return std::make_unique<LocationProviderWinrt>();
}

}  // namespace device