/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /** * This is a shared part of the OSPreferences API implementation. * It defines helper methods and public methods that are calling * platform-specific private methods. */ #include "OSPreferences.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/intl/DateTimePatternGenerator.h" #include "mozilla/intl/DateTimeFormat.h" #include "mozilla/Result.h" #include "mozilla/Services.h" #include "nsIObserverService.h" using namespace mozilla::intl; NS_IMPL_ISUPPORTS(OSPreferences, mozIOSPreferences) mozilla::StaticRefPtr OSPreferences::sInstance; // Return a new strong reference to the instance, creating it if necessary. already_AddRefed OSPreferences::GetInstanceAddRefed() { RefPtr result = sInstance; if (!result) { MOZ_ASSERT(NS_IsMainThread(), "OSPreferences should be initialized on main thread!"); if (!NS_IsMainThread()) { return nullptr; } sInstance = new OSPreferences(); result = sInstance; DebugOnly rv = Preferences::RegisterPrefixCallback( PreferenceChanged, "intl.date_time.pattern_override"); MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed."); ClearOnShutdown(&sInstance); } return result.forget(); } // Return a raw pointer to the instance: not for off-main-thread use, // because ClearOnShutdown means it could go away unexpectedly. OSPreferences* OSPreferences::GetInstance() { MOZ_ASSERT(NS_IsMainThread()); if (!sInstance) { // This will create the static instance; then we just drop the extra // reference. RefPtr result = GetInstanceAddRefed(); } return sInstance; } void OSPreferences::Refresh() { nsTArray newLocales; ReadSystemLocales(newLocales); if (mSystemLocales != newLocales) { mSystemLocales = std::move(newLocales); nsCOMPtr obs = mozilla::services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "intl:system-locales-changed", nullptr); } } } OSPreferences::~OSPreferences() { Preferences::UnregisterPrefixCallback(PreferenceChanged, "intl.date_time.pattern_override"); RemoveObservers(); } /*static*/ void OSPreferences::PreferenceChanged(const char* aPrefName, void* /* aClosure */) { if (sInstance) { sInstance->mPatternCache.Clear(); } } /** * This method should be called by every method of OSPreferences that * retrieves a locale id from external source. * * It attempts to retrieve as much of the locale ID as possible, cutting * out bits that are not understood (non-strict behavior of ICU). * * It returns true if the canonicalization was successful. */ bool OSPreferences::CanonicalizeLanguageTag(nsCString& aLoc) { return LocaleService::CanonicalizeLanguageId(aLoc); } /** * This method retrieves from mozilla::intl the best pattern for a given * date/time style. */ bool OSPreferences::GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle, DateTimeFormatStyle aTimeStyle, const nsACString& aLocale, nsACString& aRetVal) { DateTimeFormat::StyleBag style; switch (aTimeStyle) { case DateTimeFormatStyle::Short: style.time = Some(DateTimeFormat::Style::Short); break; case DateTimeFormatStyle::Medium: style.time = Some(DateTimeFormat::Style::Medium); break; case DateTimeFormatStyle::Long: style.time = Some(DateTimeFormat::Style::Long); break; case DateTimeFormatStyle::Full: style.time = Some(DateTimeFormat::Style::Full); break; case DateTimeFormatStyle::None: case DateTimeFormatStyle::Invalid: // Do nothing. break; } switch (aDateStyle) { case DateTimeFormatStyle::Short: style.date = Some(DateTimeFormat::Style::Short); break; case DateTimeFormatStyle::Medium: style.date = Some(DateTimeFormat::Style::Medium); break; case DateTimeFormatStyle::Long: style.date = Some(DateTimeFormat::Style::Long); break; case DateTimeFormatStyle::Full: style.date = Some(DateTimeFormat::Style::Full); break; case DateTimeFormatStyle::None: case DateTimeFormatStyle::Invalid: // Do nothing. break; } nsAutoCString locale; if (aLocale.IsEmpty()) { AutoTArray regionalPrefsLocales; LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales); locale.Assign(regionalPrefsLocales[0]); } else { locale.Assign(aLocale); } auto genResult = DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get()); if (genResult.isErr()) { return false; } auto generator = genResult.unwrap(); auto dfResult = DateTimeFormat::TryCreateFromStyle( MakeStringSpan(locale.get()), style, generator.get(), Nothing()); if (dfResult.isErr()) { return false; } auto df = dfResult.unwrap(); DateTimeFormat::PatternVector pattern; auto patternResult = df->GetPattern(pattern); if (patternResult.isErr()) { return false; } aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length()); return true; } /** * This method retrieves from mozilla::intl the best skeleton for a given * date/time style. * * This is useful for cases where an OS does not provide its own patterns, * but provide ability to customize the skeleton, like alter hourCycle setting. * * The returned value is a skeleton that matches the styles. */ bool OSPreferences::GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle, DateTimeFormatStyle aTimeStyle, const nsACString& aLocale, nsACString& aRetVal) { nsAutoCString pattern; if (!GetDateTimePatternForStyle(aDateStyle, aTimeStyle, aLocale, pattern)) { return false; } auto genResult = DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get()); if (genResult.isErr()) { return false; } nsAutoString patternAsUtf16 = NS_ConvertUTF8toUTF16(pattern); DateTimeFormat::SkeletonVector skeleton; auto generator = genResult.unwrap(); auto skeletonResult = generator->GetSkeleton(patternAsUtf16, skeleton); if (skeletonResult.isErr()) { return false; } aRetVal = NS_ConvertUTF16toUTF8(skeleton.begin(), skeleton.length()); return true; } /** * This method checks for preferences that override the defaults */ bool OSPreferences::OverrideDateTimePattern(DateTimeFormatStyle aDateStyle, DateTimeFormatStyle aTimeStyle, const nsACString& aLocale, nsACString& aRetVal) { const auto PrefToMaybeString = [](const char* pref) -> Maybe { nsAutoCString value; nsresult nr = Preferences::GetCString(pref, value); if (NS_FAILED(nr) || value.IsEmpty()) { return Nothing(); } return Some(std::move(value)); }; Maybe timeSkeleton; switch (aTimeStyle) { case DateTimeFormatStyle::Short: timeSkeleton = PrefToMaybeString("intl.date_time.pattern_override.time_short"); break; case DateTimeFormatStyle::Medium: timeSkeleton = PrefToMaybeString("intl.date_time.pattern_override.time_medium"); break; case DateTimeFormatStyle::Long: timeSkeleton = PrefToMaybeString("intl.date_time.pattern_override.time_long"); break; case DateTimeFormatStyle::Full: timeSkeleton = PrefToMaybeString("intl.date_time.pattern_override.time_full"); break; default: break; } Maybe dateSkeleton; switch (aDateStyle) { case DateTimeFormatStyle::Short: dateSkeleton = PrefToMaybeString("intl.date_time.pattern_override.date_short"); break; case DateTimeFormatStyle::Medium: dateSkeleton = PrefToMaybeString("intl.date_time.pattern_override.date_medium"); break; case DateTimeFormatStyle::Long: dateSkeleton = PrefToMaybeString("intl.date_time.pattern_override.date_long"); break; case DateTimeFormatStyle::Full: dateSkeleton = PrefToMaybeString("intl.date_time.pattern_override.date_full"); break; default: break; } nsAutoCString locale; if (aLocale.IsEmpty()) { AutoTArray regionalPrefsLocales; LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales); locale.Assign(regionalPrefsLocales[0]); } else { locale.Assign(aLocale); } const auto FillConnectorPattern = [&locale]( const nsAutoCString& datePattern, const nsAutoCString& timePattern) { nsAutoCString pattern; GetDateTimeConnectorPattern(nsDependentCString(locale.get()), pattern); int32_t index = pattern.Find("{1}"); if (index != kNotFound) { pattern.Replace(index, 3, datePattern); } index = pattern.Find("{0}"); if (index != kNotFound) { pattern.Replace(index, 3, timePattern); } return pattern; }; if (timeSkeleton && dateSkeleton) { aRetVal.Assign(FillConnectorPattern(*dateSkeleton, *timeSkeleton)); } else if (timeSkeleton) { if (aDateStyle != DateTimeFormatStyle::None) { nsAutoCString pattern; if (!ReadDateTimePattern(aDateStyle, DateTimeFormatStyle::None, aLocale, pattern) && !GetDateTimePatternForStyle(aDateStyle, DateTimeFormatStyle::None, aLocale, pattern)) { return false; } aRetVal.Assign(FillConnectorPattern(pattern, *timeSkeleton)); } else { aRetVal.Assign(*timeSkeleton); } } else if (dateSkeleton) { if (aTimeStyle != DateTimeFormatStyle::None) { nsAutoCString pattern; if (!ReadDateTimePattern(DateTimeFormatStyle::None, aTimeStyle, aLocale, pattern) && !GetDateTimePatternForStyle(DateTimeFormatStyle::None, aTimeStyle, aLocale, pattern)) { return false; } aRetVal.Assign(FillConnectorPattern(*dateSkeleton, pattern)); } else { aRetVal.Assign(*dateSkeleton); } } else { return false; } return true; } /** * This function is a counterpart to GetDateTimeSkeletonForStyle. * * It takes a skeleton and returns the best available pattern for a given locale * that represents the provided skeleton. * * For example: * "Hm" skeleton for "en-US" will return "H:m" */ bool OSPreferences::GetPatternForSkeleton(const nsACString& aSkeleton, const nsACString& aLocale, nsACString& aRetVal) { aRetVal.Truncate(); auto genResult = DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get()); if (genResult.isErr()) { return false; } nsAutoString skeletonAsUtf16 = NS_ConvertUTF8toUTF16(aSkeleton); DateTimeFormat::PatternVector pattern; auto generator = genResult.unwrap(); auto patternResult = generator->GetBestPattern(skeletonAsUtf16, pattern); if (patternResult.isErr()) { return false; } aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length()); return true; } /** * This function returns a pattern that should be used to join date and time * patterns into a single date/time pattern string. * * It's useful for OSes that do not provide an API to retrieve such combined * pattern. * * An example output is "{1}, {0}". */ bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale, nsACString& aRetVal) { // Check for a valid override pref and use that if present. nsAutoCString value; nsresult nr = Preferences::GetCString( "intl.date_time.pattern_override.connector_short", value); if (NS_SUCCEEDED(nr) && value.Find("{0}") != kNotFound && value.Find("{1}") != kNotFound) { aRetVal = std::move(value); return true; } auto genResult = DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get()); if (genResult.isErr()) { return false; } auto generator = genResult.unwrap(); Span result = generator->GetPlaceholderPattern(); aRetVal = NS_ConvertUTF16toUTF8(result.data(), result.size()); return true; } /** * mozIOSPreferences methods */ NS_IMETHODIMP OSPreferences::GetSystemLocales(nsTArray& aRetVal) { if (!mSystemLocales.IsEmpty()) { aRetVal = mSystemLocales.Clone(); return NS_OK; } if (ReadSystemLocales(aRetVal)) { mSystemLocales = aRetVal.Clone(); return NS_OK; } // If we failed to get the system locale, we still need // to return something because there are tests out there that // depend on system locale to be set. aRetVal.AppendElement("en-US"_ns); return NS_ERROR_FAILURE; } NS_IMETHODIMP OSPreferences::GetSystemLocale(nsACString& aRetVal) { if (!mSystemLocales.IsEmpty()) { aRetVal = mSystemLocales[0]; } else { AutoTArray locales; GetSystemLocales(locales); if (!locales.IsEmpty()) { aRetVal = locales[0]; } } return NS_OK; } NS_IMETHODIMP OSPreferences::GetRegionalPrefsLocales(nsTArray& aRetVal) { if (!mRegionalPrefsLocales.IsEmpty()) { aRetVal = mRegionalPrefsLocales.Clone(); return NS_OK; } if (ReadRegionalPrefsLocales(aRetVal)) { mRegionalPrefsLocales = aRetVal.Clone(); return NS_OK; } // If we failed to read regional prefs locales, // use system locales as last fallback. return GetSystemLocales(aRetVal); } static OSPreferences::DateTimeFormatStyle ToDateTimeFormatStyle( int32_t aTimeFormat) { switch (aTimeFormat) { // See mozIOSPreferences.idl for the integer values here. case 0: return OSPreferences::DateTimeFormatStyle::None; case 1: return OSPreferences::DateTimeFormatStyle::Short; case 2: return OSPreferences::DateTimeFormatStyle::Medium; case 3: return OSPreferences::DateTimeFormatStyle::Long; case 4: return OSPreferences::DateTimeFormatStyle::Full; } return OSPreferences::DateTimeFormatStyle::Invalid; } NS_IMETHODIMP OSPreferences::GetDateTimePattern(int32_t aDateFormatStyle, int32_t aTimeFormatStyle, const nsACString& aLocale, nsACString& aRetVal) { DateTimeFormatStyle dateStyle = ToDateTimeFormatStyle(aDateFormatStyle); if (dateStyle == DateTimeFormatStyle::Invalid) { return NS_ERROR_INVALID_ARG; } DateTimeFormatStyle timeStyle = ToDateTimeFormatStyle(aTimeFormatStyle); if (timeStyle == DateTimeFormatStyle::Invalid) { return NS_ERROR_INVALID_ARG; } // If the user is asking for None on both, date and time style, // let's exit early. if (timeStyle == DateTimeFormatStyle::None && dateStyle == DateTimeFormatStyle::None) { return NS_OK; } // If the locale is not specified, default to first regional prefs locale const nsACString* locale = &aLocale; AutoTArray rpLocales; if (aLocale.IsEmpty()) { LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales); MOZ_ASSERT(rpLocales.Length() > 0); locale = &rpLocales[0]; } // Create a cache key from the locale + style options nsAutoCString key(*locale); key.Append(':'); key.AppendInt(aDateFormatStyle); key.Append(':'); key.AppendInt(aTimeFormatStyle); nsCString pattern; if (mPatternCache.Get(key, &pattern)) { aRetVal = pattern; return NS_OK; } if (!OverrideDateTimePattern(dateStyle, timeStyle, *locale, pattern)) { if (!ReadDateTimePattern(dateStyle, timeStyle, *locale, pattern)) { if (!GetDateTimePatternForStyle(dateStyle, timeStyle, *locale, pattern)) { return NS_ERROR_FAILURE; } } } if (mPatternCache.Count() == kMaxCachedPatterns) { // Don't allow unlimited cache growth; just throw it away in the case of // pathological behavior where a page keeps requesting different formats // and locales. NS_WARNING("flushing DateTimePattern cache"); mPatternCache.Clear(); } mPatternCache.InsertOrUpdate(key, pattern); aRetVal = pattern; return NS_OK; } void OSPreferences::OverrideSkeletonHourCycle(bool aIs24Hour, nsAutoCString& aSkeleton) { if (aIs24Hour) { // If aSkeleton contains 'h' or 'K', replace with 'H' or 'k' respectively, // and delete 'a' if present. if (aSkeleton.FindChar('h') == -1 && aSkeleton.FindChar('K') == -1) { return; } for (int32_t i = 0; i < int32_t(aSkeleton.Length()); ++i) { switch (aSkeleton[i]) { case 'a': aSkeleton.Cut(i, 1); --i; break; case 'h': aSkeleton.SetCharAt('H', i); break; case 'K': aSkeleton.SetCharAt('k', i); break; } } } else { // If skeleton contains 'H' or 'k', replace with 'h' or 'K' respectively, // and add 'a' unless already present. if (aSkeleton.FindChar('H') == -1 && aSkeleton.FindChar('k') == -1) { return; } bool foundA = false; for (size_t i = 0; i < aSkeleton.Length(); ++i) { switch (aSkeleton[i]) { case 'a': foundA = true; break; case 'H': aSkeleton.SetCharAt('h', i); break; case 'k': aSkeleton.SetCharAt('K', i); break; } } if (!foundA) { aSkeleton.Append(char16_t('a')); } } }