lib/Platform/Intl/PlatformIntlAndroid.cpp (457 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "hermes/Platform/Intl/PlatformIntl.h" // Android ICU uses different package names than ICU4J, and claims // other differences. So for now, consider this impl specific to // Android. It's likely it could be made to work against the // non-Android ICU4J packages, too, if necessary, but it would take a // bit more work. #include <fbjni/fbjni.h> using namespace ::facebook; using namespace ::hermes; namespace hermes { namespace platform_intl { namespace { template <typename E = jobject> struct JArrayList : jni::JavaClass<JArrayList<E>, jni::JList<E>> { constexpr static auto kJavaDescriptor = "Ljava/util/ArrayList;"; using Super = jni::JavaClass<JArrayList<E>, jni::JList<E>>; static jni::local_ref<JArrayList<E>> create() { return Super::newInstance(); } static jni::local_ref<JArrayList<E>> create(int initialCapacity) { return Super::newInstance(initialCapacity); } bool add(jni::alias_ref<jobject> elem) { static auto addMethod = Super::javaClassStatic() ->template getMethod<jboolean(jni::alias_ref<jobject>)>("add"); return addMethod(Super::self(), elem); } }; template <typename K = jobject, typename V = jobject> struct JHashMap : jni::JavaClass<JHashMap<K, V>, jni::JMap<K, V>> { constexpr static auto kJavaDescriptor = "Ljava/util/HashMap;"; using Super = jni::JavaClass<JHashMap<K, V>, jni::JMap<K, V>>; static jni::local_ref<JHashMap<K, V>> create() { return Super::newInstance(); } void put(jni::alias_ref<jobject> key, jni::alias_ref<jobject> val) { static auto putMethod = Super::javaClassStatic() ->template getMethod<jni::alias_ref<jobject>( jni::alias_ref<jobject>, jni::alias_ref<jobject>)>("put"); putMethod(Super::self(), key, val); } }; using JLocalesList = jni::JList<jni::JString>; using JOptionsMap = jni::JMap<jni::JString, jni::JObject>; using JPartMap = jni::JMap<jni::JString, jni::JString>; using JPartsList = jni::JList<JPartMap>; jni::local_ref<jstring> stringToJava(const std::u16string &utf16) { // Work around a bug in fbjni where make_jstring returns null for empty // u16strings. // TODO(T101910387): Switch back to make_jstring once it is fixed. const auto env = jni::Environment::current(); static_assert( sizeof(jchar) == sizeof(std::u16string::value_type), "Expecting jchar to be the same size as std::u16string::CharT"); jstring result = env->NewString( reinterpret_cast<const jchar *>(utf16.c_str()), utf16.size()); FACEBOOK_JNI_THROW_PENDING_EXCEPTION(); return jni::adopt_local(result); } jni::local_ref<JLocalesList> localesToJava( std::vector<std::u16string> locales) { jni::local_ref<JArrayList<jni::JString>> ret = JArrayList<jni::JString>::create(locales.size()); for (const auto &locale : locales) { ret->add(stringToJava(locale)); } return ret; } jni::local_ref<JOptionsMap> optionsToJava(const Options &options) { auto ret = JHashMap<jni::JString, jni::JObject>::create(); for (const auto &kv : options) { jni::local_ref<jni::JObject> jvalue; if (kv.second.isBool()) { jvalue = jni::autobox(static_cast<jboolean>(kv.second.getBool())); } else if (kv.second.isNumber()) { jvalue = jni::autobox(static_cast<jdouble>(kv.second.getNumber())); } else { assert(kv.second.isString() && "Option is not valid type"); jvalue = stringToJava(kv.second.getString()); } ret->put(stringToJava(kv.first), jvalue); } return ret; } std::u16string stringFromJava(jni::alias_ref<jni::JString> result) { return result->toU16String(); } Options optionsFromJava(jni::alias_ref<JOptionsMap> result) { if (!result) { return Options(); } Options ret; for (const auto &kv : *result) { if (!kv.first || !kv.second) { // ignore nulls continue; } if (jni::JBoolean::javaClassStatic()->isAssignableFrom( kv.second->getClass())) { ret.emplace( stringFromJava(kv.first), Option(static_cast<bool>( jni::static_ref_cast<jni::JBoolean>(kv.second)->booleanValue()))); } else if (jni::JInteger::javaClassStatic()->isAssignableFrom( kv.second->getClass())) { ret.emplace( stringFromJava(kv.first), Option(static_cast<double>( jni::static_ref_cast<jni::JInteger>(kv.second)->intValue()))); } else if (jni::JDouble::javaClassStatic()->isAssignableFrom( kv.second->getClass())) { ret.emplace( stringFromJava(kv.first), Option(jni::static_ref_cast<jni::JDouble>(kv.second)->doubleValue())); } else if (jni::JString::javaClassStatic()->isAssignableFrom( kv.second->getClass())) { ret.emplace( stringFromJava(kv.first), Option( stringFromJava(jni::static_ref_cast<jni::JString>(kv.second)))); } else { // ignore mistyped value } } return ret; } // Part: Map<String, String> Part partFromJava(jni::alias_ref<JPartMap> result) { if (!result) { return Part(); } Part ret; for (const auto &kv : *result) { if (!kv.first || !kv.second) { // ignore nulls continue; } ret.emplace(stringFromJava(kv.first), stringFromJava(kv.second)); } return ret; } vm::CallResult<std::vector<std::u16string>> localesFromJava( vm::Runtime &runtime, vm::CallResult<jni::local_ref<JLocalesList>> &&result) { if (LLVM_UNLIKELY(result == vm::ExecutionStatus::EXCEPTION)) { return vm::ExecutionStatus::EXCEPTION; } std::vector<std::u16string> ret; if (!*result) { return std::vector<std::u16string>(); } for (const auto &element : **result) { ret.push_back(stringFromJava(element)); } return ret; } std::vector<Part> partsFromJava(jni::local_ref<JPartsList> &&result) { std::vector<Part> ret; if (!result) { return {}; } for (const auto &element : *result) { ret.push_back(partFromJava(element)); } return ret; } class JIntl : public jni::JavaClass<JIntl> { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/hermes/intl/Intl;"; static jni::local_ref<JLocalesList> getCanonicalLocales( jni::alias_ref<JLocalesList> locales) { static const auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<JLocalesList>( jni::alias_ref<JLocalesList> locales)>("getCanonicalLocales"); return method(javaClassStatic(), locales); } static jni::local_ref<jstring> toLocaleLowerCase( jni::alias_ref<JLocalesList> locales, jni::alias_ref<jstring> str) { auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<jstring>( jni::alias_ref<JLocalesList> locales, jni::alias_ref<jstring>)>( "toLocaleLowerCase"); return method(javaClassStatic(), locales, str); } static jni::local_ref<jstring> toLocaleUpperCase( jni::alias_ref<JLocalesList> locales, jni::alias_ref<jstring> str) { static const auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<jstring>( jni::alias_ref<JLocalesList> locales, jni::alias_ref<jstring>)>( "toLocaleUpperCase"); return method(javaClassStatic(), locales, str); } }; } // namespace vm::CallResult<std::vector<std::u16string>> getCanonicalLocales( vm::Runtime &runtime, const std::vector<std::u16string> &locales) { try { return localesFromJava( runtime, JIntl::getCanonicalLocales(localesToJava(locales))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } vm::CallResult<std::u16string> toLocaleLowerCase( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const std::u16string &str) { try { return stringFromJava( JIntl::toLocaleLowerCase(localesToJava(locales), stringToJava(str))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } vm::CallResult<std::u16string> toLocaleUpperCase( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const std::u16string &str) { try { return stringFromJava( JIntl::toLocaleUpperCase(localesToJava(locales), stringToJava(str))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } namespace { class JCollator : public jni::JavaClass<JCollator> { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/hermes/intl/Collator;"; static jni::local_ref<javaobject> create( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { return newInstance(locales, options); } static jni::local_ref<JLocalesList> supportedLocalesOf( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { static const auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<JLocalesList>( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options)>("supportedLocalesOf"); return method(javaClassStatic(), locales, options); } jni::local_ref<JOptionsMap> resolvedOptions() { static const auto method = javaClassStatic()->getMethod<jni::local_ref<JOptionsMap>()>( "resolvedOptions"); return method(self()); } double compare(jni::alias_ref<jstring> x, jni::alias_ref<jstring> y) { static const auto method = javaClassStatic() ->getMethod<double( jni::alias_ref<jstring>, jni::alias_ref<jstring>)>("compare"); return method(self(), x, y); } }; } // namespace struct Collator::Impl { jni::global_ref<JCollator> jCollator_; }; Collator::Collator() : impl_(std::make_unique<Impl>()) {} Collator::~Collator() { jni::ThreadScope::WithClassLoader([&] { impl_.reset(); }); } vm::CallResult<std::vector<std::u16string>> Collator::supportedLocalesOf( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { return localesFromJava( runtime, JCollator::supportedLocalesOf( localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } vm::ExecutionStatus Collator::initialize( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { impl_->jCollator_ = jni::make_global( JCollator::create(localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } return vm::ExecutionStatus::RETURNED; } Options Collator::resolvedOptions() noexcept { return optionsFromJava(impl_->jCollator_->resolvedOptions()); } double Collator::compare( const std::u16string &x, const std::u16string &y) noexcept { return impl_->jCollator_->compare(stringToJava(x), stringToJava(y)); } namespace { class JDateTimeFormat : public jni::JavaClass<JDateTimeFormat> { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/hermes/intl/DateTimeFormat;"; static jni::local_ref<javaobject> create( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { return newInstance(locales, options); } static jni::local_ref<JLocalesList> supportedLocalesOf( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { static const auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<JLocalesList>( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options)>("supportedLocalesOf"); return method(javaClassStatic(), locales, options); } jni::local_ref<JOptionsMap> resolvedOptions() { static const auto method = javaClassStatic()->getMethod<jni::local_ref<JOptionsMap>()>( "resolvedOptions"); return method(self()); } jni::local_ref<jstring> format(double jsTimeValue) { static const auto method = javaClassStatic()->getMethod<jni::alias_ref<jstring>(double)>("format"); return method(self(), jsTimeValue); } jni::local_ref<JPartsList> formatToParts(double jsTimeValue) { static const auto method = javaClassStatic()->getMethod<jni::alias_ref<JPartsList>(double)>( "formatToParts"); return method(self(), jsTimeValue); } }; } // namespace struct DateTimeFormat::Impl { jni::global_ref<JDateTimeFormat> jDateTimeFormat_; }; DateTimeFormat::DateTimeFormat() : impl_(std::make_unique<Impl>()) {} DateTimeFormat::~DateTimeFormat() { jni::ThreadScope::WithClassLoader([&] { impl_.reset(); }); } vm::CallResult<std::vector<std::u16string>> DateTimeFormat::supportedLocalesOf( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { return localesFromJava( runtime, JDateTimeFormat::supportedLocalesOf( localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } vm::ExecutionStatus DateTimeFormat::initialize( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { impl_->jDateTimeFormat_ = jni::make_global(JDateTimeFormat::create( localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } return vm::ExecutionStatus::RETURNED; } Options DateTimeFormat::resolvedOptions() noexcept { return optionsFromJava(impl_->jDateTimeFormat_->resolvedOptions()); } std::u16string DateTimeFormat::format(double jsTimeValue) noexcept { // I don't believe the Java logic can throw an exception (the JS // method can, but the errors all come from the Intl.cpp logic). If // I am incorrect, this will need to add a try/catch and take a // runtime to call raiseRangeError on it. This is true for all the // format methods. return stringFromJava(impl_->jDateTimeFormat_->format(jsTimeValue)); } std::vector<Part> DateTimeFormat::formatToParts(double jsTimeValue) noexcept { return partsFromJava(impl_->jDateTimeFormat_->formatToParts(jsTimeValue)); } namespace { class JNumberFormat : public jni::JavaClass<JNumberFormat> { public: static constexpr auto kJavaDescriptor = "Lcom/facebook/hermes/intl/NumberFormat;"; static jni::local_ref<javaobject> create( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { return newInstance(locales, options); } static jni::local_ref<JLocalesList> supportedLocalesOf( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options) { static const auto method = javaClassStatic() ->getStaticMethod<jni::local_ref<JLocalesList>( jni::alias_ref<JLocalesList> locales, jni::alias_ref<JOptionsMap> options)>("supportedLocalesOf"); return method(javaClassStatic(), locales, options); } jni::local_ref<JOptionsMap> resolvedOptions() { static const auto method = javaClassStatic()->getMethod<jni::local_ref<JOptionsMap>()>( "resolvedOptions"); return method(self()); } jni::local_ref<jstring> format(double jsTimeValue) { static const auto method = javaClassStatic()->getMethod<jni::alias_ref<jstring>(double)>("format"); return method(self(), jsTimeValue); } jni::local_ref<JPartsList> formatToParts(double jsTimeValue) { static const auto method = javaClassStatic()->getMethod<jni::alias_ref<JPartsList>(double)>( "formatToParts"); return method(self(), jsTimeValue); } }; } // namespace struct NumberFormat::Impl { jni::global_ref<JNumberFormat> jNumberFormat_; }; NumberFormat::NumberFormat() : impl_(std::make_unique<Impl>()) {} NumberFormat::~NumberFormat() { jni::ThreadScope::WithClassLoader([&] { impl_.reset(); }); } vm::CallResult<std::vector<std::u16string>> NumberFormat::supportedLocalesOf( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { return localesFromJava( runtime, JNumberFormat::supportedLocalesOf( localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } } vm::ExecutionStatus NumberFormat::initialize( vm::Runtime &runtime, const std::vector<std::u16string> &locales, const Options &options) noexcept { try { impl_->jNumberFormat_ = jni::make_global( JNumberFormat::create(localesToJava(locales), optionsToJava(options))); } catch (const std::exception &ex) { return runtime.raiseRangeError(ex.what()); } return vm::ExecutionStatus::RETURNED; } Options NumberFormat::resolvedOptions() noexcept { return optionsFromJava(impl_->jNumberFormat_->resolvedOptions()); } std::u16string NumberFormat::format(double number) noexcept { // I don't believe the Java logic can throw an exception (the JS // method can, but the errors all come from the Intl.cpp logic). If // I am incorrect, this will need to add a try/catch and take a // runtime to call raiseRangeError on it. This is true for all the // format methods. return stringFromJava(impl_->jNumberFormat_->format(number)); } std::vector<Part> NumberFormat::formatToParts(double number) noexcept { return partsFromJava(impl_->jNumberFormat_->formatToParts(number)); } } // namespace platform_intl } // namespace hermes