diff --git a/chrome/browser/BUILD.gn b/chrome/browser/BUILD.gn index 8e7bd7585175d1..f8817bfd805960 100644 --- a/chrome/browser/BUILD.gn +++ b/chrome/browser/BUILD.gn @@ -1624,6 +1624,7 @@ split_static_library("browser") { "//components/infobars/core", "//components/invalidation/impl", "//components/keyed_service/content", + "//components/language/content/browser", "//components/language/core/browser", "//components/metrics:call_stacks", "//components/metrics:component_metrics", diff --git a/components/language/content/browser/BUILD.gn b/components/language/content/browser/BUILD.gn index b77574817d2257..603f68c2e51c0e 100644 --- a/components/language/content/browser/BUILD.gn +++ b/components/language/content/browser/BUILD.gn @@ -35,14 +35,34 @@ source_set("language_code_locator") { ] } +static_library("browser") { + sources = [ + "geo_language_provider.cc", + "geo_language_provider.h", + ] + + deps = [ + ":language_code_locator", + "//base", + "//net", + "//services/device/public/interfaces:interfaces", + "//services/service_manager/public/cpp", + ] +} + source_set("unit_tests") { testonly = true sources = [ + "geo_language_provider_unittest.cc", "language_code_locator_unittest.cc", ] deps = [ + ":browser", ":language_code_locator", "//base", + "//base/test:test_support", + "//services/device/public/interfaces:interfaces", + "//services/service_manager/public/cpp", "//testing/gmock", "//testing/gtest", ] diff --git a/components/language/content/browser/DEPS b/components/language/content/browser/DEPS index 1c35d9ca694b70..9d2ba3a3188264 100644 --- a/components/language/content/browser/DEPS +++ b/components/language/content/browser/DEPS @@ -1,3 +1,6 @@ include_rules = [ - "+content/public/browser", + "+device/geolocation/public/interfaces", + "+net", + "+services/device/public/interfaces", + "+services/service_manager/public", ] diff --git a/components/language/content/browser/geo_language_provider.cc b/components/language/content/browser/geo_language_provider.cc new file mode 100644 index 00000000000000..b06bb4743c988c --- /dev/null +++ b/components/language/content/browser/geo_language_provider.cc @@ -0,0 +1,153 @@ +// Copyright 2017 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 "components/language/content/browser/geo_language_provider.h" + +#include "base/memory/singleton.h" +#include "base/task_scheduler/post_task.h" +#include "base/time/time.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/device/public/interfaces/constants.mojom.h" +#include "services/device/public/interfaces/public_ip_address_geolocation_provider.mojom.h" +#include "services/service_manager/public/cpp/connector.h" + +namespace language { +namespace { + +// Don't start requesting updates to IP-based approximation geolocation until +// this long after receiving the last one. +constexpr base::TimeDelta kMinUpdatePeriod = base::TimeDelta::FromDays(1); + +} // namespace + +GeoLanguageProvider::GeoLanguageProvider() + : languages_(), + creation_task_runner_(base::SequencedTaskRunnerHandle::Get()), + background_task_runner_(base::CreateSequencedTaskRunnerWithTraits( + {base::MayBlock(), base::TaskPriority::BACKGROUND, + base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) { + // Constructor is not required to run on |background_task_runner_|: + DETACH_FROM_SEQUENCE(background_sequence_checker_); +} + +GeoLanguageProvider::GeoLanguageProvider( + scoped_refptr background_task_runner) + : languages_(), + creation_task_runner_(base::SequencedTaskRunnerHandle::Get()), + background_task_runner_(background_task_runner) { + // Constructor is not required to run on |background_task_runner_|: + DETACH_FROM_SEQUENCE(background_sequence_checker_); +} + +GeoLanguageProvider::~GeoLanguageProvider() = default; + +/* static */ +GeoLanguageProvider* GeoLanguageProvider::GetInstance() { + return base::Singleton>::get(); +} + +void GeoLanguageProvider::StartUp( + std::unique_ptr service_manager_connector) { + DCHECK_CALLED_ON_VALID_SEQUENCE(creation_sequence_checker_); + + service_manager_connector_ = std::move(service_manager_connector); + // Continue startup in the background. + background_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&GeoLanguageProvider::BackgroundStartUp, + base::Unretained(this))); +} + +std::vector GeoLanguageProvider::CurrentGeoLanguages() const { + DCHECK_CALLED_ON_VALID_SEQUENCE(creation_sequence_checker_); + return languages_; +} + +void GeoLanguageProvider::BackgroundStartUp() { + // This binds background_sequence_checker_. + DCHECK_CALLED_ON_VALID_SEQUENCE(background_sequence_checker_); + + // Initialize location->language lookup library. + language_code_locator_ = std::make_unique(); + + // Make initial query. + QueryNextPosition(); +} + +void GeoLanguageProvider::BindIpGeolocationService() { + DCHECK_CALLED_ON_VALID_SEQUENCE(background_sequence_checker_); + DCHECK(!geolocation_provider_.is_bound()); + + // Bind a PublicIpAddressGeolocationProvider. + device::mojom::PublicIpAddressGeolocationProviderPtr ip_geolocation_provider; + service_manager_connector_->BindInterface( + device::mojom::kServiceName, mojo::MakeRequest(&ip_geolocation_provider)); + + net::PartialNetworkTrafficAnnotationTag partial_traffic_annotation = + net::DefinePartialNetworkTrafficAnnotation("geo_language_provider", + "network_location_request", + R"( + semantics { + sender: "GeoLanguage Provider" + } + policy { + setting: + "Users can control this feature via the translation settings " + "'Languages', 'Language', 'Offer to translate'." + chrome_policy { + DefaultGeolocationSetting { + DefaultGeolocationSetting: 2 + } + } + })"); + + // Use the PublicIpAddressGeolocationProvider to bind ip_geolocation_service_. + ip_geolocation_provider->CreateGeolocation( + static_cast( + partial_traffic_annotation), + mojo::MakeRequest(&geolocation_provider_)); + // No error handler required: If the connection is broken, QueryNextPosition + // will bind it again. +} + +void GeoLanguageProvider::QueryNextPosition() { + DCHECK_CALLED_ON_VALID_SEQUENCE(background_sequence_checker_); + + if (geolocation_provider_.encountered_error()) + geolocation_provider_.reset(); + if (!geolocation_provider_.is_bound()) + BindIpGeolocationService(); + + geolocation_provider_->QueryNextPosition(base::BindOnce( + &GeoLanguageProvider::OnIpGeolocationResponse, base::Unretained(this))); +} + +void GeoLanguageProvider::OnIpGeolocationResponse( + device::mojom::GeopositionPtr geoposition) { + DCHECK_CALLED_ON_VALID_SEQUENCE(background_sequence_checker_); + + const std::vector languages = + language_code_locator_->GetLanguageCode(geoposition->latitude, + geoposition->longitude); + + // Update current languages on UI thread. + creation_task_runner_->PostTask( + FROM_HERE, base::BindOnce(&GeoLanguageProvider::SetGeoLanguages, + base::Unretained(this), languages)); + + // Post a task to request a fresh lookup after |kMinUpdatePeriod|. + background_task_runner_->PostDelayedTask( + FROM_HERE, + base::BindOnce(&GeoLanguageProvider::QueryNextPosition, + base::Unretained(this)), + kMinUpdatePeriod); +} + +void GeoLanguageProvider::SetGeoLanguages( + const std::vector& languages) { + DCHECK_CALLED_ON_VALID_SEQUENCE(creation_sequence_checker_); + languages_ = languages; +} + +} // namespace language diff --git a/components/language/content/browser/geo_language_provider.h b/components/language/content/browser/geo_language_provider.h new file mode 100644 index 00000000000000..b361a6399267e1 --- /dev/null +++ b/components/language/content/browser/geo_language_provider.h @@ -0,0 +1,114 @@ +// Copyright 2017 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. + +#ifndef COMPONENTS_LANGUAGE_CONTENT_BROWSER_GEO_LANGUAGE_PROVIDER_H_ +#define COMPONENTS_LANGUAGE_CONTENT_BROWSER_GEO_LANGUAGE_PROVIDER_H_ + +#include + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/sequence_checker.h" +#include "base/sequenced_task_runner.h" +#include "components/language/content/browser/language_code_locator.h" +#include "device/geolocation/public/interfaces/geolocation.mojom.h" + +namespace base { +template +struct DefaultSingletonTraits; +} + +namespace service_manager { +class Connector; +} + +namespace language { +// GeoLanguageProvider is responsible for providing a "local" language derived +// from the approximate geolocation of the device based only on its public IP +// address. +// * Singleton class. Access through GetInstance(). +// * Sequencing: Must be created and used on the same sequence. +class GeoLanguageProvider { + public: + static GeoLanguageProvider* GetInstance(); + + // Call this once near browser startup. Begins ongoing geo-language updates. + // * Initializes location->language mapping in a low-priority background task. + // * Until the first IP geolocation completes, CurrentGeoLanguages() will + // return an empty list. + // |service_manager_connector| should not yet be bound to a sequence, e.g., it + // should be the result of invoking ServiceManagerConnect::Clone() on another + // connector. + void StartUp( + std::unique_ptr service_manager_connector); + + // Returns the inferred ranked list of local languages based on the most + // recently obtained approximate public-IP geolocation of the device. + // * Returns a list of BCP-47 language codes. + // * Returns an empty list in these cases: + // - StartUp() not yet called + // - Geolocation failed + // - Geolocation pending + // - Geolocation succeeded but no local language is mapped to that location + std::vector CurrentGeoLanguages() const; + + private: + friend class GeoLanguageProviderTest; + + GeoLanguageProvider(); + explicit GeoLanguageProvider( + scoped_refptr background_task_runner); + ~GeoLanguageProvider(); + friend struct base::DefaultSingletonTraits; + + // Performs actual work described in StartUp() above. + void BackgroundStartUp(); + + // Binds |ip_geolocation_service_| using a service_manager::Connector. + void BindIpGeolocationService(); + + // Requests the next available IP-based approximate geolocation from + // |ip_geolocation_service_|, binding |ip_geolocation_service_| first if + // necessary. + void QueryNextPosition(); + + // Updates the list of BCP-47 language codes that will be returned by calls to + // CurrentGeoLanguages(). + // Must be called on the UI thread. + void SetGeoLanguages(const std::vector& languages); + + // Callback for updates from |ip_geolocation_service_|. + void OnIpGeolocationResponse(device::mojom::GeopositionPtr geoposition); + + // List of BCP-47 language code inferred from public-IP geolocation. + // May be empty. See comment on CurrentGeoLanguages() above. + std::vector languages_; + + // Service manager connector for use on background_task_runner_. + std::unique_ptr service_manager_connector_; + + // Connection to the IP geolocation service. + device::mojom::GeolocationPtr geolocation_provider_; + + // Location -> Language lookup library. + std::unique_ptr language_code_locator_; + + // Runner for tasks that should run on the creation sequence. + scoped_refptr creation_task_runner_; + + // Runner for low priority background tasks. + scoped_refptr background_task_runner_; + + // Sequence checker for methods that must run on the creation sequence. + SEQUENCE_CHECKER(creation_sequence_checker_); + + // Sequence checker for background_task_runner_. + SEQUENCE_CHECKER(background_sequence_checker_); + + DISALLOW_COPY_AND_ASSIGN(GeoLanguageProvider); +}; + +} // namespace language + +#endif // COMPONENTS_LANGUAGE_CONTENT_BROWSER_GEO_LANGUAGE_PROVIDER_H_ diff --git a/components/language/content/browser/geo_language_provider_unittest.cc b/components/language/content/browser/geo_language_provider_unittest.cc new file mode 100644 index 00000000000000..bb73e4124924ce --- /dev/null +++ b/components/language/content/browser/geo_language_provider_unittest.cc @@ -0,0 +1,183 @@ +// Copyright 2017 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 "components/language/content/browser/geo_language_provider.h" + +#include +#include +#include + +#include "base/macros.h" +#include "base/test/test_mock_time_task_runner.h" +#include "base/timer/timer.h" +#include "device/geolocation/public/interfaces/geolocation.mojom.h" +#include "mojo/public/cpp/bindings/binding.h" +#include "services/device/public/interfaces/constants.mojom.h" +#include "services/device/public/interfaces/public_ip_address_geolocation_provider.mojom.h" +#include "services/service_manager/public/cpp/connector.h" +#include "services/service_manager/public/interfaces/connector.mojom.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +// Mock impl of mojom::Geolocation that allows tests to control the returned +// location. +class MockGeoLocation : public device::mojom::Geolocation { + public: + MockGeoLocation() : binding_(this) {} + // device::mojom::Geolocation implementation: + void SetHighAccuracy(bool high_accuracy) override {} + void QueryNextPosition(QueryNextPositionCallback callback) override { + ++query_next_position_called_times_; + std::move(callback).Run(position_.Clone()); + } + + void BindGeoLocation(device::mojom::GeolocationRequest request) { + binding_.Bind(std::move(request)); + } + + void MoveToLocation(float latitude, float longitude) { + position_.latitude = latitude; + position_.longitude = longitude; + } + + int query_next_position_called_times() const { + return query_next_position_called_times_; + } + + private: + int query_next_position_called_times_ = 0; + device::mojom::Geoposition position_; + mojo::Binding binding_; +}; + +// Mock impl of mojom::PublicIpAddressGeolocationProvider that binds Geolocation +// to testing impl. +class MockIpGeoLocationProvider + : public device::mojom::PublicIpAddressGeolocationProvider { + public: + explicit MockIpGeoLocationProvider(MockGeoLocation* mock_geo_location) + : mock_geo_location_(mock_geo_location), binding_(this) {} + + void Bind(mojo::ScopedMessagePipeHandle handle) { + binding_.Bind(device::mojom::PublicIpAddressGeolocationProviderRequest( + std::move(handle))); + } + + void CreateGeolocation( + const net::MutablePartialNetworkTrafficAnnotationTag& /* unused */, + device::mojom::GeolocationRequest request) override { + mock_geo_location_->BindGeoLocation(std::move(request)); + } + + private: + MockGeoLocation* mock_geo_location_; + + mojo::Binding binding_; +}; + +} // namespace + +namespace language { + +class GeoLanguageProviderTest : public testing::Test { + public: + GeoLanguageProviderTest() + : task_runner_(base::MakeRefCounted( + base::TestMockTimeTaskRunner::Type::kBoundToThread)), + scoped_context_(task_runner_.get()), + geo_language_provider_(task_runner_), + mock_ip_geo_location_provider_(&mock_geo_location_) { + service_manager::mojom::ConnectorRequest request; + connector_ = service_manager::Connector::Create(&request); + service_manager::Connector::TestApi test_api(connector_.get()); + test_api.OverrideBinderForTesting( + device::mojom::kServiceName, + device::mojom::PublicIpAddressGeolocationProvider::Name_, + base::BindRepeating(&MockIpGeoLocationProvider::Bind, + base::Unretained(&mock_ip_geo_location_provider_))); + } + + protected: + std::vector GetCurrentGeoLanguages() { + return geo_language_provider_.CurrentGeoLanguages(); + } + + void StartGeoLanguageProvider() { + geo_language_provider_.StartUp(std::move(connector_)); + } + + void MoveToLocation(float latitude, float longitude) { + mock_geo_location_.MoveToLocation(latitude, longitude); + } + + const scoped_refptr& GetTaskRunner() { + return task_runner_; + } + + int GetQueryNextPositionCalledTimes() { + return mock_geo_location_.query_next_position_called_times(); + } + + private: + scoped_refptr task_runner_; + const base::TestMockTimeTaskRunner::ScopedContext scoped_context_; + + // Object under test. + GeoLanguageProvider geo_language_provider_; + MockGeoLocation mock_geo_location_; + MockIpGeoLocationProvider mock_ip_geo_location_provider_; + std::unique_ptr connector_; +}; + +TEST_F(GeoLanguageProviderTest, GetCurrentGeoLanguages) { + // Setup a random place in Madhya Pradesh, India. + MoveToLocation(23.0, 80.0); + StartGeoLanguageProvider(); + const auto task_runner = GetTaskRunner(); + task_runner->RunUntilIdle(); + + const std::vector& result = GetCurrentGeoLanguages(); + std::vector expected_langs = {"hi", "mr", "ur"}; + EXPECT_EQ(expected_langs, result); + EXPECT_EQ(1, GetQueryNextPositionCalledTimes()); +} + +TEST_F(GeoLanguageProviderTest, NoFrequentCalls) { + // Setup a random place in Madhya Pradesh, India. + MoveToLocation(23.0, 80.0); + StartGeoLanguageProvider(); + const auto task_runner = GetTaskRunner(); + task_runner->RunUntilIdle(); + + const std::vector& result = GetCurrentGeoLanguages(); + std::vector expected_langs = {"hi", "mr", "ur"}; + EXPECT_EQ(expected_langs, result); + + task_runner->FastForwardBy(base::TimeDelta::FromHours(12)); + EXPECT_EQ(1, GetQueryNextPositionCalledTimes()); +} + +TEST_F(GeoLanguageProviderTest, ButDoCallInTheNextDay) { + // Setup a random place in Madhya Pradesh, India. + MoveToLocation(23.0, 80.0); + StartGeoLanguageProvider(); + const auto task_runner = GetTaskRunner(); + task_runner->RunUntilIdle(); + + std::vector result = GetCurrentGeoLanguages(); + std::vector expected_langs = {"hi", "mr", "ur"}; + EXPECT_EQ(expected_langs, result); + + // Move to another random place in Karnataka, India. + MoveToLocation(15.0, 75.0); + task_runner->FastForwardBy(base::TimeDelta::FromHours(25)); + EXPECT_EQ(2, GetQueryNextPositionCalledTimes()); + + result = GetCurrentGeoLanguages(); + std::vector expected_langs_2 = {"kn", "ur", "te", "mr", "ta"}; + EXPECT_EQ(expected_langs_2, result); +} + +} // namespace language