blob: 5be05c10644a27001e76c082159ad6da62913ce8 [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 <iterator>
#include <peridot/lib/fidl/clone.h>
#include <src/lib/icu_data/cpp/icu_data.h>
#include "lib/fostr/fidl/fuchsia/intl/formatting.h"
#include "lib/syslog/cpp/logger.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 modular {
using fuchsia::intl::CalendarId;
using fuchsia::intl::LocaleId;
using fuchsia::intl::Profile;
using fuchsia::intl::TemperatureUnit;
using fuchsia::intl::TimeZoneId;
using fuchsia::modular::intl::internal::RawProfileData;
using icu::UnicodeString;
using intl::ExpandLocaleId;
using intl::ExtractBcp47CalendarId;
using intl::LocaleIdToIcuLocale;
using intl::LocaleKeys;
namespace {
const std::string kDefaultTimeZoneId = "America/Los_Angeles";
// In the absence of real user preferences, make some very myopic assumptions.
RawProfileData GetDefaultRawData() {
return RawProfileData{
.language_tags = {LocaleId{.id = "en-US"}},
.time_zone_ids = {TimeZoneId{.id = kDefaultTimeZoneId}},
.calendar_ids = {CalendarId{.id = "und-u-ca-gregory"}},
.temperature_unit = TemperatureUnit::FAHRENHEIT,
};
}
// 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 modular::RawProfileData& 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}};
return fit::ok(extensions);
}
fit::result<Profile, zx_status_t> GenerateProfile(const modular::RawProfileData& 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();
mutable_calendars->insert(std::end(*mutable_calendars), std::begin(raw_data.calendar_ids),
std::end(raw_data.calendar_ids));
// Update time zones
auto mutable_time_zones = profile.mutable_time_zones();
mutable_time_zones->insert(std::end(*mutable_time_zones), std::begin(raw_data.time_zone_ids),
std::end(raw_data.time_zone_ids));
// Update rest
profile.set_temperature_unit(raw_data.temperature_unit);
// TODO(kpozin): Consider inferring temperature unit from region if missing.
return fit::ok(std::move(profile));
}
} // namespace
IntlPropertyProviderImpl::IntlPropertyProviderImpl(
fuchsia::deprecatedtimezone::TimezonePtr time_zone_client)
: intl_profile_(std::nullopt),
raw_profile_data_(std::nullopt),
time_zone_client_(std::move(time_zone_client)),
tz_watcher_binding_(this) {
Start();
}
// static
std::unique_ptr<IntlPropertyProviderImpl> IntlPropertyProviderImpl::Create(
const std::shared_ptr<sys::ServiceDirectory>& incoming_services) {
fuchsia::deprecatedtimezone::TimezonePtr time_zone_client =
incoming_services->Connect<fuchsia::deprecatedtimezone::Timezone>();
return std::make_unique<IntlPropertyProviderImpl>(std::move(time_zone_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;
}
LoadInitialValues();
}
void IntlPropertyProviderImpl::GetProfile(
fuchsia::intl::PropertyProvider::GetProfileCallback callback) {
FX_VLOGS(1) << "Received GetProfile request";
get_profile_queue_.push(std::move(callback));
ProcessGetProfileQueue();
}
void IntlPropertyProviderImpl::OnTimezoneOffsetChange(std::string time_zone_id) {
FX_VLOGS(1) << "Incoming time zone changed to " << time_zone_id;
// TODO(kpozin): When there are multiple update sources, we might want some
// sort of "mark dirty" behavior that will coalesce actual recalculations of
// of the Profile, instead going through the whole process on every change.
RawProfileData new_profile_data = CloneStruct(*raw_profile_data_);
new_profile_data.time_zone_ids = {TimeZoneId{.id = time_zone_id}};
UpdateRawData(new_profile_data);
}
zx_status_t IntlPropertyProviderImpl::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;
}
}
void IntlPropertyProviderImpl::LoadInitialValues() {
auto set_initial_data = [this](std::string time_zone_id) {
// There is no stable source for this data right now, so we use arbitrary
// US-centric defaults.
RawProfileData new_data = GetDefaultRawData();
new_data.time_zone_ids = {TimeZoneId{.id = time_zone_id}};
UpdateRawData(new_data);
// TODO: Consider setting some other error handler for non-initial errors.
time_zone_client_.set_error_handler(nullptr);
StartSettingsWatchers();
};
time_zone_client_.set_error_handler(
[set_initial_data](zx_status_t status) { set_initial_data(kDefaultTimeZoneId); });
time_zone_client_->GetTimezoneId(
[set_initial_data](std::string timezone_id) { set_initial_data(timezone_id); });
}
zx_status_t IntlPropertyProviderImpl::StartSettingsWatchers() {
// TODO(MF-168): Watch other settings.
fidl::InterfaceHandle<fuchsia::deprecatedtimezone::TimezoneWatcher> handle;
tz_watcher_binding_.Bind(handle.NewRequest());
time_zone_client_->Watch(std::move(handle));
return ZX_OK;
}
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_ok()) {
intl_profile_ = result.take_value();
} else {
FX_LOGS(WARNING) << "Couldn't generate profile: " << result.error();
return result;
}
}
return fit::ok(CloneStruct(*intl_profile_));
}
bool IntlPropertyProviderImpl::IsRawDataInitialized() { return raw_profile_data_.has_value(); }
bool IntlPropertyProviderImpl::UpdateRawData(modular::RawProfileData& new_raw_data) {
if (!IsRawDataInitialized() || (!fidl::Equals(*raw_profile_data_, new_raw_data))) {
raw_profile_data_ = std::move(new_raw_data);
// Invalidate the existing cached profile.
intl_profile_ = std::nullopt;
FX_VLOGS(1) << "Updated raw data";
NotifyOnChange();
ProcessGetProfileQueue();
return true;
}
return false;
}
void IntlPropertyProviderImpl::NotifyOnChange() {
FX_VLOGS(1) << "NotifyOnChange";
for (auto& binding : property_provider_bindings_.bindings()) {
binding->events().OnChange();
}
}
void IntlPropertyProviderImpl::ProcessGetProfileQueue() {
if (!IsRawDataInitialized()) {
FX_VLOGS(1) << "Raw data not yet initialized";
return;
}
auto profile_result = GetProfileInternal();
if (profile_result.is_error()) {
return;
}
FX_VLOGS(1) << "Processing request queue (" << get_profile_queue_.size() << ")";
while (!get_profile_queue_.empty()) {
auto& callback = get_profile_queue_.front();
callback(CloneStruct(profile_result.value()));
get_profile_queue_.pop();
}
}
} // namespace modular