| // Copyright 2019 The Fuchsia 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 "intl_property_provider_impl.h" |
| |
| #include <fuchsia/intl/cpp/fidl.h> |
| #include <fuchsia/intl/merge/cpp/fidl.h> |
| #include <fuchsia/settings/cpp/fidl.h> |
| #include <lib/syslog/cpp/macros.h> |
| #include <zircon/status.h> |
| |
| #include <iterator> |
| |
| #include <src/lib/icu_data/cpp/icu_data.h> |
| |
| #include "lib/fidl/cpp/clone.h" |
| #include "lib/fostr/fidl/fuchsia/intl/formatting.h" |
| #include "lib/fostr/fidl/fuchsia/settings/formatting.h" |
| #include "lib/zx/time.h" |
| #include "locale_util.h" |
| #include "src/lib/fxl/macros.h" |
| #include "third_party/icu/source/common/unicode/locid.h" |
| #include "third_party/icu/source/common/unicode/unistr.h" |
| #include "third_party/icu/source/i18n/unicode/calendar.h" |
| #include "third_party/icu/source/i18n/unicode/timezone.h" |
| |
| namespace intl { |
| |
| using fuchsia::intl::CalendarId; |
| using fuchsia::intl::LocaleId; |
| using fuchsia::intl::Profile; |
| using fuchsia::intl::TemperatureUnit; |
| using fuchsia::intl::TimeZoneId; |
| using fuchsia::settings::HourCycle; |
| using intl::ExpandLocaleId; |
| using intl::ExtractBcp47CalendarId; |
| using intl::LocaleIdToIcuLocale; |
| using intl::LocaleKeys; |
| |
| namespace { |
| |
| // Returns the default settings for the merged data. |
| fuchsia::intl::merge::Data DataDefaults() { |
| fuchsia::intl::merge::Data ret; |
| ret.set_language_tags({LocaleId{.id = "en-US"}}); |
| ret.set_time_zone_ids({TimeZoneId{.id = "America/Los_Angeles"}}); |
| ret.set_calendar_ids({CalendarId{.id = "und-u-ca-gregory"}}); |
| ret.set_temperature_unit(TemperatureUnit::FAHRENHEIT); |
| return ret; |
| } |
| |
| // Returns the basis from which final values for RawProfileData are obtained. |
| fuchsia::intl::merge::Data GetDefaultRawData( |
| const std::optional<fuchsia::intl::merge::Data>& prototype) { |
| static const fuchsia::intl::merge::Data default_merge_data = DataDefaults(); |
| return prototype.has_value() ? fidl::Clone(*prototype) : fidl::Clone(default_merge_data); |
| } |
| |
| // Collect key-value pairs of Unicode locale properties that will be applied to |
| // each locale ID. |
| fit::result<std::map<std::string, std::string>, zx_status_t> GetUnicodeExtensionsForDenormalization( |
| const fuchsia::intl::merge::Data& raw_data) { |
| auto primary_calendar_id_result = ExtractBcp47CalendarId(raw_data.calendar_ids()[0]); |
| if (primary_calendar_id_result.is_error()) { |
| FX_LOGS(ERROR) << "Bad calendar ID: " << raw_data.calendar_ids()[0]; |
| return fit::error(primary_calendar_id_result.error()); |
| } |
| const std::string& primary_calendar_id = primary_calendar_id_result.value(); |
| |
| const std::string& primary_tz_id_iana = raw_data.time_zone_ids()[0].id; |
| const char* primary_tz_id = |
| uloc_toUnicodeLocaleType(LocaleKeys::kTimeZone.c_str(), primary_tz_id_iana.c_str()); |
| if (primary_tz_id == nullptr) { |
| FX_LOGS(ERROR) << "Bad time zone ID: " << primary_tz_id_iana; |
| return fit::error(ZX_ERR_INVALID_ARGS); |
| } |
| |
| std::map<std::string, std::string> extensions{{LocaleKeys::kCalendar, primary_calendar_id}, |
| {LocaleKeys::kTimeZone, primary_tz_id}}; |
| if (raw_data.has_hour_cycle()) { |
| switch (raw_data.hour_cycle()) { |
| case HourCycle::H12: |
| extensions[LocaleKeys::kHourCycle] = "h12"; |
| break; |
| case HourCycle::H23: |
| extensions[LocaleKeys::kHourCycle] = "h23"; |
| break; |
| default: |
| // Unknown. |
| break; |
| } |
| } |
| return fit::ok(extensions); |
| } |
| |
| fit::result<Profile, zx_status_t> GenerateProfile(const fuchsia::intl::merge::Data& raw_data) { |
| if (raw_data.language_tags().empty()) { |
| FX_LOGS(ERROR) << "GenerateProfile called with empty raw locale IDs"; |
| return fit::error(ZX_ERR_INVALID_ARGS); |
| } |
| |
| auto unicode_extensions_result = GetUnicodeExtensionsForDenormalization(raw_data); |
| if (unicode_extensions_result.is_error()) { |
| return fit::error(unicode_extensions_result.error()); |
| } |
| |
| const auto unicode_extensions = unicode_extensions_result.value(); |
| |
| std::vector<icu::Locale> icu_locales; |
| for (const auto& locale_id : raw_data.language_tags()) { |
| auto icu_locale_result = LocaleIdToIcuLocale(locale_id, unicode_extensions); |
| if (icu_locale_result.is_error()) { |
| FX_LOGS(WARNING) << "Failed to build locale for " << locale_id; |
| } else { |
| icu_locales.push_back(icu_locale_result.value()); |
| } |
| } |
| |
| Profile profile; |
| // Update locales |
| for (auto& icu_locale : icu_locales) { |
| fit::result<LocaleId, zx_status_t> locale_id_result = ExpandLocaleId(icu_locale); |
| if (locale_id_result.is_ok()) { |
| profile.mutable_locales()->push_back(locale_id_result.value()); |
| } |
| // Errors are logged inside ExpandLocaleId |
| } |
| |
| if (!profile.has_locales() || profile.locales().empty()) { |
| FX_LOGS(ERROR) << "No valid locales could be built"; |
| return fit::error(ZX_ERR_INVALID_ARGS); |
| } |
| |
| // Update calendars |
| auto* mutable_calendars = profile.mutable_calendars(); |
| const auto& calendars = raw_data.calendar_ids(); |
| mutable_calendars->insert(std::end(*mutable_calendars), std::begin(calendars), |
| std::end(calendars)); |
| |
| // Update time zones |
| auto* mutable_time_zones = profile.mutable_time_zones(); |
| const auto& time_zones = raw_data.time_zone_ids(); |
| mutable_time_zones->insert(std::end(*mutable_time_zones), std::begin(time_zones), |
| std::end(time_zones)); |
| |
| // Update rest |
| if (raw_data.has_temperature_unit()) { |
| profile.set_temperature_unit(raw_data.temperature_unit()); |
| } |
| // TODO(kpozin): Consider inferring temperature unit from region if missing. |
| |
| return fit::ok(std::move(profile)); |
| } |
| |
| // Extracts just the timezone ID from the setting object. If the setting is not |
| // well-formed or not valid, no value is returned. |
| std::optional<std::string> TimeZoneIdFrom(const fuchsia::settings::IntlSettings& setting) { |
| if (!setting.has_time_zone_id()) { |
| return std::nullopt; |
| } |
| return setting.time_zone_id().id; |
| } |
| |
| // Merges the timezone settings into new profile data. |
| void MergeTimeZone(const std::optional<std::string>& timezone_id, |
| fuchsia::intl::merge::Data* new_profile_data) { |
| if (!timezone_id.has_value()) { |
| return; |
| } |
| // Merge the new value with the old. |
| new_profile_data->set_time_zone_ids({TimeZoneId{.id = *timezone_id}}); |
| } |
| |
| // Merges the intl settings into the new profile data. |
| void MergeIntl(const fuchsia::settings::IntlSettings& intl_settings, |
| fuchsia::intl::merge::Data* new_profile_data) { |
| // Replace the old settings with the new. |
| new_profile_data->set_temperature_unit(intl_settings.temperature_unit()); |
| // Do not touch the current locale settings if setui tells us there are no languages |
| // set. |
| const std::vector<fuchsia::intl::LocaleId>& locale_ids = intl_settings.locales(); |
| if (!locale_ids.empty()) { |
| new_profile_data->set_language_tags(locale_ids); |
| } else { |
| FX_LOGS(WARNING) |
| << "fuchsia.settings.Intl returned locale settings with no locales; this is not a valid " |
| "fuchsia.intl.Profile; not touching the current language settings and proceeding."; |
| } |
| if (intl_settings.has_hour_cycle()) { |
| new_profile_data->set_hour_cycle(intl_settings.hour_cycle()); |
| } |
| } |
| |
| // Sinks the setting into new_profile_data, by overwriting the content of new_profile_data with the |
| // content provided by setting. |
| void Merge(const fuchsia::settings::IntlSettings& setting, |
| fuchsia::intl::merge::Data* new_profile_data) { |
| FX_CHECK(new_profile_data != nullptr); |
| const auto timezone_id = TimeZoneIdFrom(setting); |
| MergeTimeZone(timezone_id, new_profile_data); |
| MergeIntl(setting, new_profile_data); |
| } |
| |
| // Load initial ICU data if this hasn't been done already. |
| // |
| // TODO(kpozin): Eventually, this should solely be the responsibility of the client component that |
| // links `IntlPropertyProviderImpl`, which has a better idea of what parameters ICU should be |
| // initialized with. |
| zx_status_t InitializeIcuIfNeeded() { |
| // It's okay if something else in the same process has already initialized |
| // ICU. |
| zx_status_t status = icu_data::Initialize(); |
| switch (status) { |
| case ZX_OK: |
| case ZX_ERR_ALREADY_BOUND: |
| return ZX_OK; |
| default: |
| return status; |
| } |
| } |
| |
| } // namespace |
| |
| IntlPropertyProviderImpl::IntlPropertyProviderImpl(fuchsia::settings::IntlPtr settings_client_) |
| : intl_profile_(std::nullopt), |
| raw_profile_data_(std::nullopt), |
| settings_client_(std::move(settings_client_)) { |
| Start(); |
| } |
| |
| // static |
| std::unique_ptr<IntlPropertyProviderImpl> IntlPropertyProviderImpl::Create( |
| const std::shared_ptr<sys::ServiceDirectory>& incoming_services) { |
| fuchsia::settings::IntlPtr client = incoming_services->Connect<fuchsia::settings::Intl>(); |
| return std::make_unique<IntlPropertyProviderImpl>(std::move(client)); |
| } |
| |
| fidl::InterfaceRequestHandler<fuchsia::intl::PropertyProvider> IntlPropertyProviderImpl::GetHandler( |
| async_dispatcher_t* dispatcher) { |
| return property_provider_bindings_.GetHandler(this, dispatcher); |
| } |
| |
| void IntlPropertyProviderImpl::Start() { |
| if (InitializeIcuIfNeeded() != ZX_OK) { |
| FX_LOGS(ERROR) << "Failed to initialize ICU data"; |
| return; |
| } |
| settings_client_.set_error_handler([](zx_status_t status) { |
| FX_LOGS(ERROR) << "settings_client error: " << zx_status_get_string(status); |
| }); |
| StartSettingsWatcher(); |
| } |
| |
| void IntlPropertyProviderImpl::GetProfile( |
| fuchsia::intl::PropertyProvider::GetProfileCallback callback) { |
| FX_VLOGS(1) << "Received GetProfile request"; |
| get_profile_queue_.push(std::move(callback)); |
| ProcessProfileRequests(); |
| } |
| |
| void IntlPropertyProviderImpl::StartSettingsWatcher() { |
| settings_client_->Watch([this](fuchsia::settings::IntlSettings settings) { |
| FX_VLOGS(2) << "New settings value: " << settings; |
| fuchsia::intl::merge::Data new_profile_data = GetDefaultRawData(raw_profile_data_); |
| Merge(settings, &new_profile_data); |
| UpdateRawData(std::move(new_profile_data)); |
| StartSettingsWatcher(); |
| }); |
| } |
| |
| fit::result<Profile, zx_status_t> IntlPropertyProviderImpl::GetProfileInternal() { |
| if (!intl_profile_) { |
| Profile profile; |
| if (!IsRawDataInitialized()) { |
| return fit::error(ZX_ERR_SHOULD_WAIT); |
| } |
| auto result = GenerateProfile(*raw_profile_data_); |
| if (result.is_error()) { |
| FX_LOGS(WARNING) << "Couldn't generate profile: " << result.error(); |
| return result; |
| } |
| intl_profile_ = result.take_value(); |
| } |
| return fit::ok(fidl::Clone(*intl_profile_)); |
| } |
| |
| bool IntlPropertyProviderImpl::IsRawDataInitialized() { return raw_profile_data_.has_value(); } |
| |
| bool IntlPropertyProviderImpl::UpdateRawData(fuchsia::intl::merge::Data new_raw_data) { |
| if (IsRawDataInitialized() && fidl::Equals(*raw_profile_data_, new_raw_data)) { |
| return false; |
| } |
| raw_profile_data_ = std::move(new_raw_data); |
| // Invalidate the existing cached profile. |
| intl_profile_ = std::nullopt; |
| FX_VLOGS(1) << "Updated raw data"; |
| for (const auto& binding : property_provider_bindings_.bindings()) { |
| binding->events().OnChange(); |
| } |
| ProcessProfileRequests(); |
| return true; |
| } |
| |
| void IntlPropertyProviderImpl::ProcessProfileRequests() { |
| if (!IsRawDataInitialized()) { |
| FX_VLOGS(1) << "Raw data not yet initialized"; |
| return; |
| } |
| |
| auto profile_result = GetProfileInternal(); |
| if (profile_result.is_error()) { |
| FX_VLOGS(1) << "Profile not updated: error was: " << profile_result.error(); |
| return; |
| } |
| |
| FX_VLOGS(1) << "Processing request queue (" << get_profile_queue_.size() << ")"; |
| while (!get_profile_queue_.empty()) { |
| auto& callback = get_profile_queue_.front(); |
| auto var = fidl::Clone(profile_result.value()); |
| callback(std::move(var)); |
| get_profile_queue_.pop(); |
| } |
| } |
| |
| } // namespace intl |