1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6 /**
7 * This is a shared part of the OSPreferences API implementation.
8 * It defines helper methods and public methods that are calling
9 * platform-specific private methods.
10 */
11
12 #include "OSPreferences.h"
13
14 #include "mozilla/ClearOnShutdown.h"
15 #include "mozilla/intl/DateTimePatternGenerator.h"
16 #include "mozilla/intl/DateTimeFormat.h"
17 #include "mozilla/Result.h"
18 #include "mozilla/Services.h"
19 #include "nsIObserverService.h"
20
21 using namespace mozilla::intl;
22
23 NS_IMPL_ISUPPORTS(OSPreferences, mozIOSPreferences)
24
25 mozilla::StaticRefPtr<OSPreferences> OSPreferences::sInstance;
26
27 // Return a new strong reference to the instance, creating it if necessary.
GetInstanceAddRefed()28 already_AddRefed<OSPreferences> OSPreferences::GetInstanceAddRefed() {
29 RefPtr<OSPreferences> result = sInstance;
30 if (!result) {
31 MOZ_ASSERT(NS_IsMainThread(),
32 "OSPreferences should be initialized on main thread!");
33 if (!NS_IsMainThread()) {
34 return nullptr;
35 }
36 sInstance = new OSPreferences();
37 result = sInstance;
38
39 DebugOnly<nsresult> rv = Preferences::RegisterPrefixCallback(
40 PreferenceChanged, "intl.date_time.pattern_override");
41 MOZ_ASSERT(NS_SUCCEEDED(rv), "Adding observers failed.");
42
43 ClearOnShutdown(&sInstance);
44 }
45 return result.forget();
46 }
47
48 // Return a raw pointer to the instance: not for off-main-thread use,
49 // because ClearOnShutdown means it could go away unexpectedly.
GetInstance()50 OSPreferences* OSPreferences::GetInstance() {
51 MOZ_ASSERT(NS_IsMainThread());
52 if (!sInstance) {
53 // This will create the static instance; then we just drop the extra
54 // reference.
55 RefPtr<OSPreferences> result = GetInstanceAddRefed();
56 }
57 return sInstance;
58 }
59
Refresh()60 void OSPreferences::Refresh() {
61 nsTArray<nsCString> newLocales;
62 ReadSystemLocales(newLocales);
63
64 if (mSystemLocales != newLocales) {
65 mSystemLocales = std::move(newLocales);
66 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
67 if (obs) {
68 obs->NotifyObservers(nullptr, "intl:system-locales-changed", nullptr);
69 }
70 }
71 }
72
~OSPreferences()73 OSPreferences::~OSPreferences() {
74 Preferences::UnregisterPrefixCallback(PreferenceChanged,
75 "intl.date_time.pattern_override");
76 RemoveObservers();
77 }
78
79 /*static*/
PreferenceChanged(const char * aPrefName,void *)80 void OSPreferences::PreferenceChanged(const char* aPrefName,
81 void* /* aClosure */) {
82 if (sInstance) {
83 sInstance->mPatternCache.Clear();
84 }
85 }
86
87 /**
88 * This method should be called by every method of OSPreferences that
89 * retrieves a locale id from external source.
90 *
91 * It attempts to retrieve as much of the locale ID as possible, cutting
92 * out bits that are not understood (non-strict behavior of ICU).
93 *
94 * It returns true if the canonicalization was successful.
95 */
CanonicalizeLanguageTag(nsCString & aLoc)96 bool OSPreferences::CanonicalizeLanguageTag(nsCString& aLoc) {
97 return LocaleService::CanonicalizeLanguageId(aLoc);
98 }
99
100 /**
101 * This method retrieves from mozilla::intl the best pattern for a given
102 * date/time style.
103 */
GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)104 bool OSPreferences::GetDateTimePatternForStyle(DateTimeFormatStyle aDateStyle,
105 DateTimeFormatStyle aTimeStyle,
106 const nsACString& aLocale,
107 nsACString& aRetVal) {
108 DateTimeFormat::StyleBag style;
109
110 switch (aTimeStyle) {
111 case DateTimeFormatStyle::Short:
112 style.time = Some(DateTimeFormat::Style::Short);
113 break;
114 case DateTimeFormatStyle::Medium:
115 style.time = Some(DateTimeFormat::Style::Medium);
116 break;
117 case DateTimeFormatStyle::Long:
118 style.time = Some(DateTimeFormat::Style::Long);
119 break;
120 case DateTimeFormatStyle::Full:
121 style.time = Some(DateTimeFormat::Style::Full);
122 break;
123 case DateTimeFormatStyle::None:
124 case DateTimeFormatStyle::Invalid:
125 // Do nothing.
126 break;
127 }
128
129 switch (aDateStyle) {
130 case DateTimeFormatStyle::Short:
131 style.date = Some(DateTimeFormat::Style::Short);
132 break;
133 case DateTimeFormatStyle::Medium:
134 style.date = Some(DateTimeFormat::Style::Medium);
135 break;
136 case DateTimeFormatStyle::Long:
137 style.date = Some(DateTimeFormat::Style::Long);
138 break;
139 case DateTimeFormatStyle::Full:
140 style.date = Some(DateTimeFormat::Style::Full);
141 break;
142 case DateTimeFormatStyle::None:
143 case DateTimeFormatStyle::Invalid:
144 // Do nothing.
145 break;
146 }
147
148 nsAutoCString locale;
149 if (aLocale.IsEmpty()) {
150 AutoTArray<nsCString, 10> regionalPrefsLocales;
151 LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
152 locale.Assign(regionalPrefsLocales[0]);
153 } else {
154 locale.Assign(aLocale);
155 }
156
157 auto genResult =
158 DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
159 if (genResult.isErr()) {
160 return false;
161 }
162 auto generator = genResult.unwrap();
163
164 auto dfResult = DateTimeFormat::TryCreateFromStyle(
165 MakeStringSpan(locale.get()), style, generator.get(), Nothing());
166 if (dfResult.isErr()) {
167 return false;
168 }
169 auto df = dfResult.unwrap();
170
171 DateTimeFormat::PatternVector pattern;
172 auto patternResult = df->GetPattern(pattern);
173 if (patternResult.isErr()) {
174 return false;
175 }
176
177 aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
178 return true;
179 }
180
181 /**
182 * This method retrieves from mozilla::intl the best skeleton for a given
183 * date/time style.
184 *
185 * This is useful for cases where an OS does not provide its own patterns,
186 * but provide ability to customize the skeleton, like alter hourCycle setting.
187 *
188 * The returned value is a skeleton that matches the styles.
189 */
GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)190 bool OSPreferences::GetDateTimeSkeletonForStyle(DateTimeFormatStyle aDateStyle,
191 DateTimeFormatStyle aTimeStyle,
192 const nsACString& aLocale,
193 nsACString& aRetVal) {
194 nsAutoCString pattern;
195 if (!GetDateTimePatternForStyle(aDateStyle, aTimeStyle, aLocale, pattern)) {
196 return false;
197 }
198
199 auto genResult =
200 DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
201 if (genResult.isErr()) {
202 return false;
203 }
204
205 nsAutoString patternAsUtf16 = NS_ConvertUTF8toUTF16(pattern);
206 DateTimeFormat::SkeletonVector skeleton;
207 auto generator = genResult.unwrap();
208 auto skeletonResult = generator->GetSkeleton(patternAsUtf16, skeleton);
209 if (skeletonResult.isErr()) {
210 return false;
211 }
212
213 aRetVal = NS_ConvertUTF16toUTF8(skeleton.begin(), skeleton.length());
214 return true;
215 }
216
217 /**
218 * This method checks for preferences that override the defaults
219 */
OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,DateTimeFormatStyle aTimeStyle,const nsACString & aLocale,nsACString & aRetVal)220 bool OSPreferences::OverrideDateTimePattern(DateTimeFormatStyle aDateStyle,
221 DateTimeFormatStyle aTimeStyle,
222 const nsACString& aLocale,
223 nsACString& aRetVal) {
224 const auto PrefToMaybeString = [](const char* pref) -> Maybe<nsAutoCString> {
225 nsAutoCString value;
226 nsresult nr = Preferences::GetCString(pref, value);
227 if (NS_FAILED(nr) || value.IsEmpty()) {
228 return Nothing();
229 }
230 return Some(std::move(value));
231 };
232
233 Maybe<nsAutoCString> timeSkeleton;
234 switch (aTimeStyle) {
235 case DateTimeFormatStyle::Short:
236 timeSkeleton =
237 PrefToMaybeString("intl.date_time.pattern_override.time_short");
238 break;
239 case DateTimeFormatStyle::Medium:
240 timeSkeleton =
241 PrefToMaybeString("intl.date_time.pattern_override.time_medium");
242 break;
243 case DateTimeFormatStyle::Long:
244 timeSkeleton =
245 PrefToMaybeString("intl.date_time.pattern_override.time_long");
246 break;
247 case DateTimeFormatStyle::Full:
248 timeSkeleton =
249 PrefToMaybeString("intl.date_time.pattern_override.time_full");
250 break;
251 default:
252 break;
253 }
254
255 Maybe<nsAutoCString> dateSkeleton;
256 switch (aDateStyle) {
257 case DateTimeFormatStyle::Short:
258 dateSkeleton =
259 PrefToMaybeString("intl.date_time.pattern_override.date_short");
260 break;
261 case DateTimeFormatStyle::Medium:
262 dateSkeleton =
263 PrefToMaybeString("intl.date_time.pattern_override.date_medium");
264 break;
265 case DateTimeFormatStyle::Long:
266 dateSkeleton =
267 PrefToMaybeString("intl.date_time.pattern_override.date_long");
268 break;
269 case DateTimeFormatStyle::Full:
270 dateSkeleton =
271 PrefToMaybeString("intl.date_time.pattern_override.date_full");
272 break;
273 default:
274 break;
275 }
276
277 nsAutoCString locale;
278 if (aLocale.IsEmpty()) {
279 AutoTArray<nsCString, 10> regionalPrefsLocales;
280 LocaleService::GetInstance()->GetRegionalPrefsLocales(regionalPrefsLocales);
281 locale.Assign(regionalPrefsLocales[0]);
282 } else {
283 locale.Assign(aLocale);
284 }
285
286 const auto FillConnectorPattern = [&locale](
287 const nsAutoCString& datePattern,
288 const nsAutoCString& timePattern) {
289 nsAutoCString pattern;
290 GetDateTimeConnectorPattern(nsDependentCString(locale.get()), pattern);
291 int32_t index = pattern.Find("{1}");
292 if (index != kNotFound) {
293 pattern.Replace(index, 3, datePattern);
294 }
295 index = pattern.Find("{0}");
296 if (index != kNotFound) {
297 pattern.Replace(index, 3, timePattern);
298 }
299 return pattern;
300 };
301
302 if (timeSkeleton && dateSkeleton) {
303 aRetVal.Assign(FillConnectorPattern(*dateSkeleton, *timeSkeleton));
304 } else if (timeSkeleton) {
305 if (aDateStyle != DateTimeFormatStyle::None) {
306 nsAutoCString pattern;
307 if (!ReadDateTimePattern(aDateStyle, DateTimeFormatStyle::None, aLocale,
308 pattern) &&
309 !GetDateTimePatternForStyle(aDateStyle, DateTimeFormatStyle::None,
310 aLocale, pattern)) {
311 return false;
312 }
313 aRetVal.Assign(FillConnectorPattern(pattern, *timeSkeleton));
314 } else {
315 aRetVal.Assign(*timeSkeleton);
316 }
317 } else if (dateSkeleton) {
318 if (aTimeStyle != DateTimeFormatStyle::None) {
319 nsAutoCString pattern;
320 if (!ReadDateTimePattern(DateTimeFormatStyle::None, aTimeStyle, aLocale,
321 pattern) &&
322 !GetDateTimePatternForStyle(DateTimeFormatStyle::None, aTimeStyle,
323 aLocale, pattern)) {
324 return false;
325 }
326 aRetVal.Assign(FillConnectorPattern(*dateSkeleton, pattern));
327 } else {
328 aRetVal.Assign(*dateSkeleton);
329 }
330 } else {
331 return false;
332 }
333
334 return true;
335 }
336
337 /**
338 * This function is a counterpart to GetDateTimeSkeletonForStyle.
339 *
340 * It takes a skeleton and returns the best available pattern for a given locale
341 * that represents the provided skeleton.
342 *
343 * For example:
344 * "Hm" skeleton for "en-US" will return "H:m"
345 */
GetPatternForSkeleton(const nsACString & aSkeleton,const nsACString & aLocale,nsACString & aRetVal)346 bool OSPreferences::GetPatternForSkeleton(const nsACString& aSkeleton,
347 const nsACString& aLocale,
348 nsACString& aRetVal) {
349 aRetVal.Truncate();
350
351 auto genResult =
352 DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
353 if (genResult.isErr()) {
354 return false;
355 }
356
357 nsAutoString skeletonAsUtf16 = NS_ConvertUTF8toUTF16(aSkeleton);
358 DateTimeFormat::PatternVector pattern;
359 auto generator = genResult.unwrap();
360 auto patternResult = generator->GetBestPattern(skeletonAsUtf16, pattern);
361 if (patternResult.isErr()) {
362 return false;
363 }
364
365 aRetVal = NS_ConvertUTF16toUTF8(pattern.begin(), pattern.length());
366 return true;
367 }
368
369 /**
370 * This function returns a pattern that should be used to join date and time
371 * patterns into a single date/time pattern string.
372 *
373 * It's useful for OSes that do not provide an API to retrieve such combined
374 * pattern.
375 *
376 * An example output is "{1}, {0}".
377 */
GetDateTimeConnectorPattern(const nsACString & aLocale,nsACString & aRetVal)378 bool OSPreferences::GetDateTimeConnectorPattern(const nsACString& aLocale,
379 nsACString& aRetVal) {
380 // Check for a valid override pref and use that if present.
381 nsAutoCString value;
382 nsresult nr = Preferences::GetCString(
383 "intl.date_time.pattern_override.connector_short", value);
384 if (NS_SUCCEEDED(nr) && value.Find("{0}") != kNotFound &&
385 value.Find("{1}") != kNotFound) {
386 aRetVal = std::move(value);
387 return true;
388 }
389
390 auto genResult =
391 DateTimePatternGenerator::TryCreate(PromiseFlatCString(aLocale).get());
392 if (genResult.isErr()) {
393 return false;
394 }
395
396 auto generator = genResult.unwrap();
397 Span<const char16_t> result = generator->GetPlaceholderPattern();
398 aRetVal = NS_ConvertUTF16toUTF8(result.data(), result.size());
399 return true;
400 }
401
402 /**
403 * mozIOSPreferences methods
404 */
405 NS_IMETHODIMP
GetSystemLocales(nsTArray<nsCString> & aRetVal)406 OSPreferences::GetSystemLocales(nsTArray<nsCString>& aRetVal) {
407 if (!mSystemLocales.IsEmpty()) {
408 aRetVal = mSystemLocales.Clone();
409 return NS_OK;
410 }
411
412 if (ReadSystemLocales(aRetVal)) {
413 mSystemLocales = aRetVal.Clone();
414 return NS_OK;
415 }
416
417 // If we failed to get the system locale, we still need
418 // to return something because there are tests out there that
419 // depend on system locale to be set.
420 aRetVal.AppendElement("en-US"_ns);
421 return NS_ERROR_FAILURE;
422 }
423
424 NS_IMETHODIMP
GetSystemLocale(nsACString & aRetVal)425 OSPreferences::GetSystemLocale(nsACString& aRetVal) {
426 if (!mSystemLocales.IsEmpty()) {
427 aRetVal = mSystemLocales[0];
428 } else {
429 AutoTArray<nsCString, 10> locales;
430 GetSystemLocales(locales);
431 if (!locales.IsEmpty()) {
432 aRetVal = locales[0];
433 }
434 }
435 return NS_OK;
436 }
437
438 NS_IMETHODIMP
GetRegionalPrefsLocales(nsTArray<nsCString> & aRetVal)439 OSPreferences::GetRegionalPrefsLocales(nsTArray<nsCString>& aRetVal) {
440 if (!mRegionalPrefsLocales.IsEmpty()) {
441 aRetVal = mRegionalPrefsLocales.Clone();
442 return NS_OK;
443 }
444
445 if (ReadRegionalPrefsLocales(aRetVal)) {
446 mRegionalPrefsLocales = aRetVal.Clone();
447 return NS_OK;
448 }
449
450 // If we failed to read regional prefs locales,
451 // use system locales as last fallback.
452 return GetSystemLocales(aRetVal);
453 }
454
ToDateTimeFormatStyle(int32_t aTimeFormat)455 static OSPreferences::DateTimeFormatStyle ToDateTimeFormatStyle(
456 int32_t aTimeFormat) {
457 switch (aTimeFormat) {
458 // See mozIOSPreferences.idl for the integer values here.
459 case 0:
460 return OSPreferences::DateTimeFormatStyle::None;
461 case 1:
462 return OSPreferences::DateTimeFormatStyle::Short;
463 case 2:
464 return OSPreferences::DateTimeFormatStyle::Medium;
465 case 3:
466 return OSPreferences::DateTimeFormatStyle::Long;
467 case 4:
468 return OSPreferences::DateTimeFormatStyle::Full;
469 }
470 return OSPreferences::DateTimeFormatStyle::Invalid;
471 }
472
473 NS_IMETHODIMP
GetDateTimePattern(int32_t aDateFormatStyle,int32_t aTimeFormatStyle,const nsACString & aLocale,nsACString & aRetVal)474 OSPreferences::GetDateTimePattern(int32_t aDateFormatStyle,
475 int32_t aTimeFormatStyle,
476 const nsACString& aLocale,
477 nsACString& aRetVal) {
478 DateTimeFormatStyle dateStyle = ToDateTimeFormatStyle(aDateFormatStyle);
479 if (dateStyle == DateTimeFormatStyle::Invalid) {
480 return NS_ERROR_INVALID_ARG;
481 }
482 DateTimeFormatStyle timeStyle = ToDateTimeFormatStyle(aTimeFormatStyle);
483 if (timeStyle == DateTimeFormatStyle::Invalid) {
484 return NS_ERROR_INVALID_ARG;
485 }
486
487 // If the user is asking for None on both, date and time style,
488 // let's exit early.
489 if (timeStyle == DateTimeFormatStyle::None &&
490 dateStyle == DateTimeFormatStyle::None) {
491 return NS_OK;
492 }
493
494 // If the locale is not specified, default to first regional prefs locale
495 const nsACString* locale = &aLocale;
496 AutoTArray<nsCString, 10> rpLocales;
497 if (aLocale.IsEmpty()) {
498 LocaleService::GetInstance()->GetRegionalPrefsLocales(rpLocales);
499 MOZ_ASSERT(rpLocales.Length() > 0);
500 locale = &rpLocales[0];
501 }
502
503 // Create a cache key from the locale + style options
504 nsAutoCString key(*locale);
505 key.Append(':');
506 key.AppendInt(aDateFormatStyle);
507 key.Append(':');
508 key.AppendInt(aTimeFormatStyle);
509
510 nsCString pattern;
511 if (mPatternCache.Get(key, &pattern)) {
512 aRetVal = pattern;
513 return NS_OK;
514 }
515
516 if (!OverrideDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
517 if (!ReadDateTimePattern(dateStyle, timeStyle, *locale, pattern)) {
518 if (!GetDateTimePatternForStyle(dateStyle, timeStyle, *locale, pattern)) {
519 return NS_ERROR_FAILURE;
520 }
521 }
522 }
523
524 if (mPatternCache.Count() == kMaxCachedPatterns) {
525 // Don't allow unlimited cache growth; just throw it away in the case of
526 // pathological behavior where a page keeps requesting different formats
527 // and locales.
528 NS_WARNING("flushing DateTimePattern cache");
529 mPatternCache.Clear();
530 }
531 mPatternCache.InsertOrUpdate(key, pattern);
532
533 aRetVal = pattern;
534 return NS_OK;
535 }
536
OverrideSkeletonHourCycle(bool aIs24Hour,nsAutoCString & aSkeleton)537 void OSPreferences::OverrideSkeletonHourCycle(bool aIs24Hour,
538 nsAutoCString& aSkeleton) {
539 if (aIs24Hour) {
540 // If aSkeleton contains 'h' or 'K', replace with 'H' or 'k' respectively,
541 // and delete 'a' if present.
542 if (aSkeleton.FindChar('h') == -1 && aSkeleton.FindChar('K') == -1) {
543 return;
544 }
545 for (int32_t i = 0; i < int32_t(aSkeleton.Length()); ++i) {
546 switch (aSkeleton[i]) {
547 case 'a':
548 aSkeleton.Cut(i, 1);
549 --i;
550 break;
551 case 'h':
552 aSkeleton.SetCharAt('H', i);
553 break;
554 case 'K':
555 aSkeleton.SetCharAt('k', i);
556 break;
557 }
558 }
559 } else {
560 // If skeleton contains 'H' or 'k', replace with 'h' or 'K' respectively,
561 // and add 'a' unless already present.
562 if (aSkeleton.FindChar('H') == -1 && aSkeleton.FindChar('k') == -1) {
563 return;
564 }
565 bool foundA = false;
566 for (size_t i = 0; i < aSkeleton.Length(); ++i) {
567 switch (aSkeleton[i]) {
568 case 'a':
569 foundA = true;
570 break;
571 case 'H':
572 aSkeleton.SetCharAt('h', i);
573 break;
574 case 'k':
575 aSkeleton.SetCharAt('K', i);
576 break;
577 }
578 }
579 if (!foundA) {
580 aSkeleton.Append(char16_t('a'));
581 }
582 }
583 }
584