blob: 24e6f2555a23eb191dcfbbb8d29d5e215283365b [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 <iterator>
#include <src/lib/icu_data/cpp/icu_data.h>
#include <src/modular/lib/fidl/clone.h>
#include "fuchsia/setui/cpp/fidl.h"
#include "lib/fostr/fidl/fuchsia/intl/formatting.h"
#include "locale_util.h"
#include "src/lib/fxl/macros.h"
#include "src/lib/syslog/cpp/logger.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::HourCycle;
using fuchsia::modular::intl::internal::RawProfileData;
using intl::ExpandLocaleId;
using intl::ExtractBcp47CalendarId;
using intl::LocaleIdToIcuLocale;
using intl::LocaleKeys;
namespace {
const std::string kDefaultTimeZoneId = "America/Los_Angeles";
// Returns the basis from which final values for RawProfileData are obtained.
RawProfileData GetDefaultRawData(const std::optional<RawProfileData>& prototype) {
return prototype.has_value() ? CloneStruct(*prototype)
: 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}};
if (raw_data.hour_cycle != nullptr) {
switch (raw_data.hour_cycle->setting) {
case fuchsia::setui::HourCycle::H12:
extensions[LocaleKeys::kHourCycle] = "h12";
break;
case fuchsia::setui::HourCycle::H23:
extensions[LocaleKeys::kHourCycle] = "h23";
break;
default:
// If we ever add a different hour cycle setting, e.g. "locale default", this will work.
// So, a bit of future-proofing here. I wonder if it's going to be such.
break;
}
}
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));
}
// 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::setui::SettingsObject& setting) {
if (setting.setting_type != fuchsia::setui::SettingType::TIME_ZONE) {
// Should never happen since the Watch/Listen protocol ensures the setting matches.
return std::nullopt;
}
const fuchsia::setui::TimeZoneInfo& timezone_info = setting.data.time_zone_value();
if (timezone_info.current == nullptr) {
return std::nullopt;
}
const auto* timezone = setting.data.time_zone_value().current.get();
if (timezone_info.current->id.empty()) {
// Weird data in the timezone field causes us to not update anything.
return std::nullopt;
}
return std::string(timezone->id);
}
// Safely extracts intl settings from a union.
std::optional<fuchsia::setui::IntlSettings> IntlSettingsFrom(
const fuchsia::setui::SettingsObject& setting) {
if (!setting.data.is_intl()) {
return std::nullopt;
}
return setting.data.intl();
}
// Merges the timezone settings into new profile data.
void MergeTimeZone(const std::optional<std::string>& timezone_id,
RawProfileData* new_profile_data) {
if (!timezone_id.has_value()) {
return;
}
// Merge the new value with the old.
new_profile_data->time_zone_ids = {TimeZoneId{.id = *timezone_id}};
}
// Merges the intl settings into the new profile data.
void MergeIntl(const std::optional<fuchsia::setui::IntlSettings>& intl_settings,
RawProfileData* new_profile_data) {
if (!intl_settings.has_value()) {
return;
}
// Replace the old settings with the new.
switch (intl_settings->temperature_unit) {
case fuchsia::setui::TemperatureUnit::CELSIUS:
new_profile_data->temperature_unit = fuchsia::intl::TemperatureUnit::CELSIUS;
break;
case fuchsia::setui::TemperatureUnit::FAHRENHEIT:
new_profile_data->temperature_unit = fuchsia::intl::TemperatureUnit::FAHRENHEIT;
break;
default:
FX_LOGS(WARNING) << "fuchsia.setui gave us an unknown temperature unit enum value: "
<< static_cast<uint32_t>(intl_settings->temperature_unit);
}
if (!intl_settings->locales.empty()) {
// Do not touch the current locale settings if setui tells us there are no languages
// set.
new_profile_data->language_tags.clear();
for (const auto& locale : intl_settings->locales) {
new_profile_data->language_tags.emplace_back(fuchsia::intl::LocaleId{.id = locale});
}
} else {
FX_LOGS(WARNING)
<< "fuchsia.setui returned locale settings with no locales; this is not a valid "
"fuchsia.intl.Profile; not touching the current language settings and proceeding.";
}
// Setui does not have a way to leave hour cycle setting to the locale, so we always set it
// here. However, if an option comes in to set it, we can do that too.
new_profile_data->hour_cycle = std::make_unique<HourCycle>(HourCycle{
.setting = 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::setui::SettingsObject& setting, RawProfileData* new_profile_data) {
FX_CHECK(new_profile_data != nullptr);
// Using the same Notify function for all settings type, here we process on
// a case by case basis.
const auto setting_type = setting.setting_type;
switch (setting_type) {
case fuchsia::setui::SettingType::TIME_ZONE: {
const auto timezone_id = TimeZoneIdFrom(setting);
MergeTimeZone(timezone_id, new_profile_data);
} break;
case fuchsia::setui::SettingType::INTL: {
const auto intl = IntlSettingsFrom(setting);
MergeIntl(intl, new_profile_data);
} break;
default:
// The default branch should, in theory, not trigger since in the setup code we subscribe
// only to specific SettingsType values. If it does, it could be a bug on the server side,
// or could be that we have a new setting interest but have not registered to process it.
// operator<< is not implemented for SettingType, so we just print the corresponding
// unsigned value. See FIDL definition of the enum for the value mapping.
FX_LOGS(WARNING) << "Got unexpected setting type: " << static_cast<uint32_t>(setting_type);
break;
}
}
// 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;
}
}
// Used to initialize the first, "empty" timezone info.
fuchsia::setui::SettingsObject InitialSettingsObject() {
auto data = fuchsia::setui::SettingData::New();
data->set_time_zone_value(fuchsia::setui::TimeZoneInfo{});
fuchsia::setui::SettingsObject object{
.setting_type = fuchsia::setui::SettingType::TIME_ZONE,
};
FX_CHECK(data->Clone(&object.data) == ZX_OK);
return object;
}
} // namespace
IntlPropertyProviderImpl::IntlPropertyProviderImpl(fuchsia::setui::SetUiServicePtr setui_client)
: intl_profile_(std::nullopt),
raw_profile_data_(std::nullopt),
setui_client_(std::move(setui_client)),
setting_listener_binding_(this),
setting_listener_binding_intl_(this),
initial_settings_object_(InitialSettingsObject()) {
Start();
}
// static
std::unique_ptr<IntlPropertyProviderImpl> IntlPropertyProviderImpl::Create(
const std::shared_ptr<sys::ServiceDirectory>& incoming_services) {
fuchsia::setui::SetUiServicePtr setui_client =
incoming_services->Connect<fuchsia::setui::SetUiService>();
return std::make_unique<IntlPropertyProviderImpl>(std::move(setui_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::LoadInitialValues() {
auto set_initial_data = [this](const fuchsia::setui::SettingsObject& setting) {
NotifyInternal(setting);
// TODO(kpozin): Consider setting some other error handler for non-initial errors.
setui_client_.set_error_handler(nullptr);
StartSettingsWatcher(setting.setting_type);
};
setui_client_.set_error_handler(
[this, set_initial_data](zx_status_t status __attribute__((unused))) {
// Sending an initial data request with empty time zone will initialize the time
// zone and other settings to their default empty values.
set_initial_data(initial_settings_object_);
});
auto watch_callback = [set_initial_data](fuchsia::setui::SettingsObject setting) {
set_initial_data(setting);
};
setui_client_->Watch(fuchsia::setui::SettingType::TIME_ZONE, watch_callback);
setui_client_->Watch(fuchsia::setui::SettingType::INTL, watch_callback);
}
void IntlPropertyProviderImpl::StartSettingsWatcher(fuchsia::setui::SettingType type) {
fidl::InterfaceHandle<fuchsia::setui::SettingListener> handle;
auto& binding = (type == fuchsia::setui::SettingType::TIME_ZONE) ? setting_listener_binding_
: setting_listener_binding_intl_;
binding.Bind(handle.NewRequest());
setui_client_->Listen(type, std::move(handle));
}
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)) {
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";
NotifyOnChange();
ProcessGetProfileQueue();
return true;
}
void IntlPropertyProviderImpl::Notify(fuchsia::setui::SettingsObject setting) {
NotifyInternal(setting);
}
void IntlPropertyProviderImpl::NotifyInternal(const fuchsia::setui::SettingsObject& setting) {
RawProfileData new_profile_data = GetDefaultRawData(raw_profile_data_);
Merge(setting, &new_profile_data);
UpdateRawData(new_profile_data);
}
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()) {
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 = CloneStruct(profile_result.value());
callback(std::move(var));
get_profile_queue_.pop();
}
}
} // namespace modular