blob: 7d1f76b428fc4a5f167cea343c091fc73ab6952a [file] [log] [blame]
// 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