Revision control

Copy as Markdown

// Copyright (C) 2026 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qlocale_p.h"
#include "qdatetime.h"
#include "qjniobject.h"
#include "qreadwritelock.h"
#include "qstringlist.h"
#include "qvariant.h"
#include <QtCore/private/qjnihelpers_p.h>
QT_BEGIN_NAMESPACE
#ifndef QT_NO_SYSTEMLOCALE
Q_DECLARE_JNI_CLASS(Locale, "java/util/Locale")
Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources")
Q_DECLARE_JNI_CLASS(Configuration, "android/content/res/Configuration")
Q_DECLARE_JNI_CLASS(LocaleList, "android/os/LocaleList")
Q_DECLARE_JNI_CLASS(DateFormat, "java/text/DateFormat")
namespace {
using namespace Qt::StringLiterals;
using namespace QtJniTypes;
struct LocaleInfo
{
QLocale::Language language = QLocale::AnyLanguage;
QLocale::Script script = QLocale::AnyScript;
QLocale::Territory territory = QLocale::AnyTerritory;
QString timeFormatShort;
QString timeFormatLong;
QString dateFormatShort;
QString dateFormatLong;
};
struct QSystemLocaleData
{
void readLocaleFromJava();
LocaleInfo snapshot()
{
QReadLocker locker(&lock);
return localeInfo;
}
QReadWriteLock lock;
LocaleInfo localeInfo;
};
// Translate the Java SimpleDateFormat tokens Qt doesn't accept to Qt's
// (E/EE/EEE → ddd, EEEE → dddd, y/yyy+ → yyyy; a → aP to keep the locale's
// own AM/PM casing rather than force lower-case). Other tokens pass through.
QString qtPatternFromJava(QStringView pattern)
{
QString out;
out.reserve(pattern.size());
bool inQuoted = false;
while (!pattern.isEmpty()) {
const QChar ch = pattern.front();
qsizetype consumed = 1;
if (ch == u'\'') {
inQuoted = !inQuoted;
out.append(ch);
} else if (inQuoted) {
out.append(ch);
} else if (ch == u'E' || ch == u'y' || ch == u'a') {
while (consumed < pattern.size() && pattern[consumed] == ch)
++consumed;
if (ch == u'E')
out.append(consumed >= 4 ? "dddd"_L1 : "ddd"_L1);
else if (ch == u'y')
out.append(consumed == 2 ? "yy"_L1 : "yyyy"_L1);
else // 'a'
out.append("aP"_L1);
} else {
out.append(ch);
}
pattern = pattern.sliced(consumed);
}
return out;
}
// Qt pattern via java.text.DateFormat.get{Date,Time}Instance(style, locale).
QString patternForStyle(QSystemLocale::QueryType type, const Locale &locale)
{
constexpr jint SHORT = 3;
constexpr jint MEDIUM = 2;
constexpr jint FULL = 0;
const char *factory = nullptr;
jint style = 0;
switch (type) {
case QSystemLocale::DateFormatShort: factory = "getDateInstance"; style = SHORT; break;
case QSystemLocale::DateFormatLong: factory = "getDateInstance"; style = FULL; break;
case QSystemLocale::TimeFormatShort: factory = "getTimeInstance"; style = SHORT; break;
case QSystemLocale::TimeFormatLong: factory = "getTimeInstance"; style = MEDIUM; break;
default: Q_UNREACHABLE(); break;
}
const auto dateFormat = DateFormat::callStaticMethod<DateFormat>(factory, style, locale);
return qtPatternFromJava(dateFormat.callMethod<QString>("toPattern"));
}
void QSystemLocaleData::readLocaleFromJava()
{
const QJniObject context = QtAndroidPrivate::context();
const Locale androidLocale = [context]{
if (context.isValid()) {
const auto resources = context.callMethod<Resources>("getResources");
const auto configuration = resources.callMethod<Configuration>("getConfiguration");
return configuration.getField<Locale>("locale");
}
return Locale::callStaticMethod<Locale>("getDefault");
}();
LocaleInfo updated;
updated.language = QLocale::codeToLanguage(androidLocale.callMethod<QString>("getLanguage"));
updated.script = QLocale::codeToScript(androidLocale.callMethod<QString>("getScript"));
updated.territory = QLocale::codeToTerritory(androidLocale.callMethod<QString>("getCountry"));
// QLocale does not yet support variants (QTBUG-81051)
// updated.variant = androidLocale.callMethod<QString>("getVariant");
updated.dateFormatShort = patternForStyle(QSystemLocale::DateFormatShort, androidLocale);
updated.dateFormatLong = patternForStyle(QSystemLocale::DateFormatLong, androidLocale);
updated.timeFormatShort = patternForStyle(QSystemLocale::TimeFormatShort, androidLocale);
updated.timeFormatLong = patternForStyle(QSystemLocale::TimeFormatLong, androidLocale);
// QTBUG-145605: locale-correct date/time gluing is possible with
// java.text.DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale).
// QLocale handles DateTimeFormat{Short,Long} gluing, so they are deliberately left out.
QWriteLocker locker(&lock);
localeInfo = updated;
}
Q_GLOBAL_STATIC(QSystemLocaleData, qSystemLocaleData)
} // unnamed namespace
QLocale QSystemLocale::fallbackLocale() const
{
QSystemLocaleData *d = qSystemLocaleData();
if (!d)
return QLocale(QLocale::C);
const LocaleInfo info = d->snapshot();
return QLocale(info.language, info.script, info.territory);
}
QVariant QSystemLocale::query(QueryType type, QVariant &&in) const
{
Q_UNUSED(in);
QSystemLocaleData *d = qSystemLocaleData();
if (!d)
return QVariant();
if (type == LocaleChanged) {
d->readLocaleFromJava();
return QVariant();
}
const LocaleInfo info = d->snapshot();
switch (type) {
case LanguageId:
return QVariant::fromValue(static_cast<int>(info.language));
case TerritoryId:
return QVariant::fromValue(static_cast<int>(info.territory));
case ScriptId:
return QVariant::fromValue(static_cast<int>(info.script));
case Collation:
return QLocale(info.language, info.script, info.territory).name();
case TimeFormatShort:
return info.timeFormatShort;
case TimeFormatLong:
return info.timeFormatLong;
case DateFormatShort:
return info.dateFormatShort;
case DateFormatLong:
return info.dateFormatLong;
case UILanguages: {
const LocaleList localeList = LocaleList::callStaticMethod<LocaleList>("getDefault");
if (!localeList.isValid())
return QVariant();
QString tags = localeList.callMethod<QString>("toLanguageTags");
// Some devices wrap the list in [] - strip those if present.
if (tags.startsWith(u'[') && tags.endsWith(u']'))
tags = tags.mid(1, tags.length() - 2);
return tags.split(u',');
}
case LocaleChanged:
Q_UNREACHABLE(); // handled before the switch
default:
break;
}
return QVariant();
}
#endif // QT_NO_SYSTEMLOCALE
QT_END_NAMESPACE