Decouple the TZ and ICU backends for timezone data
The TZ backend was instantiating an ICU backend to look up display names, which entailed locking of the lazy-evaluated ICU member. Instead, break out how ICU looks up display names into the new timezone_locale feature's code, so that it can be shared between the two backends. Linux thus only needs to build one backend. Task-number: QTBUG-115158 Change-Id: I37ebc9a30f1c0ab6fd32d45dffa9f21355fb868d Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>bb10
parent
e813acbb02
commit
bd01008bca
|
|
@ -897,7 +897,7 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_timezone AND UNIX AND NOT AN
|
|||
|
||||
qt_internal_extend_target(Core
|
||||
CONDITION
|
||||
QT_FEATURE_icu AND QT_FEATURE_timezone AND NOT ANDROID AND NOT APPLE
|
||||
QT_FEATURE_icu AND QT_FEATURE_timezone AND NOT UNIX
|
||||
SOURCES
|
||||
time/qtimezoneprivate_icu.cpp
|
||||
)
|
||||
|
|
|
|||
|
|
@ -860,7 +860,8 @@ qt_feature("timezone_locale" PRIVATE
|
|||
SECTION "Utilities"
|
||||
LABEL "QTimeZone"
|
||||
PURPOSE "Provides support for localized time-zone display names."
|
||||
DISABLE ON # Implementation is currently incomplete, so leave turned off
|
||||
CONDITION
|
||||
QT_FEATURE_timezone AND ( ( UNIX AND NOT APPLE AND NOT ANDROID ) OR QT_FEATURE_icu )
|
||||
)
|
||||
qt_feature("datetimeparser" PRIVATE
|
||||
SECTION "Utilities"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,9 @@
|
|||
#include <private/qtimezonelocale_p.h>
|
||||
#include <private/qtimezoneprivate_p.h>
|
||||
|
||||
#if !QT_CONFIG(icu) // Use data generated from CLDR:
|
||||
#if !QT_CONFIG(icu)
|
||||
# include <private/qdatetime_p.h>
|
||||
// Use data generated from CLDR:
|
||||
# include <private/qtimezonelocale_data_p.h>
|
||||
# include <private/qtimezoneprivate_data_p.h>
|
||||
#endif
|
||||
|
|
@ -12,9 +14,90 @@
|
|||
QT_BEGIN_NAMESPACE
|
||||
|
||||
#if QT_CONFIG(icu) // Get data from ICU:
|
||||
namespace {
|
||||
|
||||
// Convert TimeType and NameType into ICU UCalendarDisplayNameType
|
||||
constexpr UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType)
|
||||
{
|
||||
// TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType
|
||||
// For now, treat Generic as Standard
|
||||
switch (nameType) {
|
||||
case QTimeZone::OffsetName:
|
||||
Q_UNREACHABLE(); // Callers of ucalTimeZoneDisplayName() should take care of OffsetName.
|
||||
case QTimeZone::ShortName:
|
||||
return timeType == QTimeZone::DaylightTime ? UCAL_SHORT_DST : UCAL_SHORT_STANDARD;
|
||||
case QTimeZone::DefaultName:
|
||||
case QTimeZone::LongName:
|
||||
return timeType == QTimeZone::DaylightTime ? UCAL_DST : UCAL_STANDARD;
|
||||
}
|
||||
Q_UNREACHABLE_RETURN(UCAL_STANDARD);
|
||||
}
|
||||
|
||||
} // nameless namespace
|
||||
|
||||
namespace QtTimeZoneLocale {
|
||||
|
||||
// Qt wrapper around ucal_getTimeZoneDisplayName()
|
||||
// Used directly by ICU backend; indirectly by TZ (see below).
|
||||
QString ucalTimeZoneDisplayName(UCalendar *ucal,
|
||||
QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QByteArray &localeCode)
|
||||
{
|
||||
constexpr int32_t BigNameLength = 50;
|
||||
int32_t size = BigNameLength;
|
||||
QString result(size, Qt::Uninitialized);
|
||||
auto dst = [&result]() { return reinterpret_cast<UChar *>(result.data()); };
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
const UCalendarDisplayNameType utype = ucalDisplayNameType(timeType, nameType);
|
||||
|
||||
// size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status)
|
||||
size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
|
||||
dst(), size, &status);
|
||||
|
||||
// If overflow, then resize and retry
|
||||
if (size > BigNameLength || status == U_BUFFER_OVERFLOW_ERROR) {
|
||||
result.resize(size);
|
||||
status = U_ZERO_ERROR;
|
||||
size = ucal_getTimeZoneDisplayName(ucal, utype, localeCode.constData(),
|
||||
dst(), size, &status);
|
||||
}
|
||||
|
||||
if (!U_SUCCESS(status))
|
||||
return QString();
|
||||
|
||||
// Resize and return:
|
||||
result.resize(size);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // QtTimeZoneLocale
|
||||
|
||||
// Used by TZ backends when ICU is available:
|
||||
QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
|
||||
QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QLocale &locale) const
|
||||
{
|
||||
Q_UNUSED(atMSecsSinceEpoch);
|
||||
// TODO: use CLDR data for the offset name.
|
||||
// No ICU API for offset formats, so fall back to our ISO one, even if
|
||||
// locale isn't C:
|
||||
if (nameType == QTimeZone::OffsetName)
|
||||
return isoOffsetFormat(offsetFromUtc);
|
||||
|
||||
const QString id = QString::fromUtf8(m_id);
|
||||
const QByteArray loc = locale.name().toUtf8();
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
UCalendar *ucal = ucal_open(reinterpret_cast<const UChar *>(id.data()), id.size(),
|
||||
loc.constData(), UCAL_DEFAULT, &status);
|
||||
if (ucal && U_SUCCESS(status)) {
|
||||
auto tidier = qScopeGuard([ucal]() { ucal_close(ucal); });
|
||||
return QtTimeZoneLocale::ucalTimeZoneDisplayName(ucal, timeType, nameType, loc);
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
#else // No ICU, use QTZ[LP}_data_p.h data for feature timezone_locale.
|
||||
namespace {
|
||||
using namespace QtTimeZoneLocale; // QTZL_data_p.h
|
||||
|
|
@ -24,6 +107,20 @@ using namespace QtTimeZoneCldr; // QTZP_data_p.h
|
|||
// Accessors for the QTZP_data_p.h
|
||||
|
||||
} // nameless namespace
|
||||
|
||||
QString QTimeZonePrivate::localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
|
||||
QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QLocale &locale) const
|
||||
{
|
||||
Q_ASSERT(nameType != QTimeZone::OffsetName || locale.language() != QLocale::C);
|
||||
// Get data from QTZ[LP]_data_p.h
|
||||
|
||||
Q_UNUSED(atMSecsSinceEpoch);
|
||||
Q_UNUSED(offsetFromUtc);
|
||||
Q_UNUSED(timeType);
|
||||
return QString();
|
||||
}
|
||||
#endif // ICU
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
|
|
|||
|
|
@ -14,18 +14,31 @@
|
|||
//
|
||||
// We mean it.
|
||||
//
|
||||
#include <private/qglobal_p.h>
|
||||
|
||||
#include <QtCore/qstring.h>
|
||||
#include <QtCore/qtimezone.h>
|
||||
|
||||
#if QT_CONFIG(icu)
|
||||
#include <unicode/ucal.h>
|
||||
#endif
|
||||
|
||||
QT_REQUIRE_CONFIG(timezone);
|
||||
QT_REQUIRE_CONFIG(timezone_locale);
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
namespace QtTimeZoneLocale {
|
||||
#if QT_CONFIG(icu)
|
||||
QString ucalTimeZoneDisplayName(UCalendar *ucal, QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QString &localeCode);
|
||||
#else
|
||||
// Define data types for QTZL_data_p.h
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
||||
#endif // QTIMEZONELOCALE_P_H
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
|
||||
#include "qtimezone.h"
|
||||
#include "qtimezoneprivate_p.h"
|
||||
#if QT_CONFIG(timezone_locale)
|
||||
# include "qtimezonelocale_p.h"
|
||||
#endif
|
||||
#include "qtimezoneprivate_data_p.h"
|
||||
|
||||
#include <qdatastream.h>
|
||||
|
|
@ -181,7 +184,11 @@ QString QTimeZonePrivate::displayName(qint64 atMSecsSinceEpoch,
|
|||
|
||||
QTimeZone::TimeType timeType
|
||||
= tran.daylightTimeOffset != 0 ? QTimeZone::DaylightTime : QTimeZone::StandardTime;
|
||||
#if QT_CONFIG(timezone_locale)
|
||||
return localeName(atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
|
||||
#else
|
||||
return displayName(timeType, nameType, locale);
|
||||
#endif
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
|
@ -190,10 +197,14 @@ QString QTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|||
QTimeZone::NameType nameType,
|
||||
const QLocale &locale) const
|
||||
{
|
||||
if (nameType == QTimeZone::OffsetName && isDataLocale(locale)) {
|
||||
const Data tran = data(timeType);
|
||||
if (tran.atMSecsSinceEpoch != invalidMSecs())
|
||||
const Data tran = data(timeType);
|
||||
if (tran.atMSecsSinceEpoch != invalidMSecs()) {
|
||||
if (nameType == QTimeZone::OffsetName && isDataLocale(locale))
|
||||
return isoOffsetFormat(tran.offsetFromUtc);
|
||||
|
||||
#if QT_CONFIG(timezone_locale)
|
||||
return localeName(tran.atMSecsSinceEpoch, tran.offsetFromUtc, timeType, nameType, locale);
|
||||
#endif
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "qtimezone.h"
|
||||
#include "qtimezoneprivate_p.h"
|
||||
#include "qtimezonelocale_p.h"
|
||||
|
||||
#include <unicode/ucal.h>
|
||||
|
||||
|
|
@ -22,27 +23,6 @@ QT_BEGIN_NAMESPACE
|
|||
|
||||
// ICU utilities
|
||||
|
||||
// Convert TimeType and NameType into ICU UCalendarDisplayNameType
|
||||
static UCalendarDisplayNameType ucalDisplayNameType(QTimeZone::TimeType timeType, QTimeZone::NameType nameType)
|
||||
{
|
||||
// TODO ICU C UCalendarDisplayNameType does not support full set of C++ TimeZone::EDisplayType
|
||||
switch (nameType) {
|
||||
case QTimeZone::ShortName :
|
||||
case QTimeZone::OffsetName :
|
||||
if (timeType == QTimeZone::DaylightTime)
|
||||
return UCAL_SHORT_DST;
|
||||
// Includes GenericTime
|
||||
return UCAL_SHORT_STANDARD;
|
||||
case QTimeZone::DefaultName :
|
||||
case QTimeZone::LongName :
|
||||
if (timeType == QTimeZone::DaylightTime)
|
||||
return UCAL_DST;
|
||||
// Includes GenericTime
|
||||
return UCAL_STANDARD;
|
||||
}
|
||||
return UCAL_STANDARD;
|
||||
}
|
||||
|
||||
// Qt wrapper around ucal_getDefaultTimeZone()
|
||||
static QByteArray ucalDefaultTimeZoneId()
|
||||
{
|
||||
|
|
@ -69,44 +49,6 @@ static QByteArray ucalDefaultTimeZoneId()
|
|||
return QByteArray();
|
||||
}
|
||||
|
||||
// Qt wrapper around ucal_getTimeZoneDisplayName()
|
||||
static QString ucalTimeZoneDisplayName(UCalendar *ucal, QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QString &localeCode)
|
||||
{
|
||||
int32_t size = 50;
|
||||
QString result(size, Qt::Uninitialized);
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
|
||||
// size = ucal_getTimeZoneDisplayName(cal, type, locale, result, resultLength, status)
|
||||
size = ucal_getTimeZoneDisplayName(ucal,
|
||||
ucalDisplayNameType(timeType, nameType),
|
||||
localeCode.toUtf8(),
|
||||
reinterpret_cast<UChar *>(result.data()),
|
||||
size,
|
||||
&status);
|
||||
|
||||
// If overflow, then resize and retry
|
||||
if (status == U_BUFFER_OVERFLOW_ERROR) {
|
||||
result.resize(size);
|
||||
status = U_ZERO_ERROR;
|
||||
size = ucal_getTimeZoneDisplayName(ucal,
|
||||
ucalDisplayNameType(timeType, nameType),
|
||||
localeCode.toUtf8(),
|
||||
reinterpret_cast<UChar *>(result.data()),
|
||||
size,
|
||||
&status);
|
||||
}
|
||||
|
||||
// If successful on first or second go, resize and return
|
||||
if (U_SUCCESS(status)) {
|
||||
result.resize(size);
|
||||
return result;
|
||||
}
|
||||
|
||||
return QString();
|
||||
}
|
||||
|
||||
// Qt wrapper around ucal_get() for offsets
|
||||
static bool ucalOffsetsAtTime(UCalendar *m_ucal, qint64 atMSecsSinceEpoch,
|
||||
int *utcOffset, int *dstOffset)
|
||||
|
|
@ -203,13 +145,11 @@ static QTimeZonePrivate::Data ucalTimeZoneTransition(UCalendar *m_ucal,
|
|||
tran.offsetFromUtc = utc + dst;
|
||||
tran.standardTimeOffset = utc;
|
||||
tran.daylightTimeOffset = dst;
|
||||
// TODO No ICU API, use short name instead
|
||||
if (dst == 0)
|
||||
tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, QTimeZone::StandardTime,
|
||||
QTimeZone::ShortName, QLocale().name());
|
||||
else
|
||||
tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, QTimeZone::DaylightTime,
|
||||
QTimeZone::ShortName, QLocale().name());
|
||||
// TODO No ICU API, use short name as abbreviation.
|
||||
QTimeZone::TimeType timeType = dst == 0 ? QTimeZone::StandardTime : QTimeZone::DaylightTime;
|
||||
using namespace QtTimeZoneLocale;
|
||||
tran.abbreviation = ucalTimeZoneDisplayName(m_ucal, timeType,
|
||||
QTimeZone::ShortName, QLocale().name());
|
||||
return tran;
|
||||
}
|
||||
#endif // U_ICU_VERSION_SHORT
|
||||
|
|
@ -317,6 +257,7 @@ QString QIcuTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|||
}
|
||||
// Technically this may be suspect, if locale isn't QLocale(), since that's
|
||||
// what we used when constructing m_ucal; does ICU cope with inconsistency ?
|
||||
using namespace QtTimeZoneLocale;
|
||||
return ucalTimeZoneDisplayName(m_ucal, timeType, nameType, locale.name());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@ public:
|
|||
return QByteArrayLiteral("UTC");
|
||||
}
|
||||
|
||||
#if QT_CONFIG(timezone_locale)
|
||||
private:
|
||||
// Defined in qtimezonelocale.cpp
|
||||
QString localeName(qint64 atMSecsSinceEpoch, int offsetFromUtc,
|
||||
QTimeZone::TimeType timeType,
|
||||
QTimeZone::NameType nameType,
|
||||
const QLocale &locale) const;
|
||||
#endif // L10n helpers.
|
||||
|
||||
protected:
|
||||
QByteArray m_id;
|
||||
};
|
||||
|
|
@ -223,7 +232,7 @@ private:
|
|||
// TODO: shuffle (almost reverse) order of and rework #if-ery here to use #elif
|
||||
// and match the #if-ery in each of QTZ's newBackendTimeZone() cascades for
|
||||
// backend selection.
|
||||
#if QT_CONFIG(icu)
|
||||
#if QT_CONFIG(icu) && !defined(Q_OS_UNIX)
|
||||
class Q_AUTOTEST_EXPORT QIcuTimeZonePrivate final : public QTimeZonePrivate
|
||||
{
|
||||
public:
|
||||
|
|
@ -266,7 +275,7 @@ private:
|
|||
|
||||
UCalendar *m_ucal;
|
||||
};
|
||||
#endif // ICU
|
||||
#endif // ICU not on Unix.
|
||||
|
||||
#if defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN) && !defined(Q_OS_ANDROID)
|
||||
struct QTzTransitionTime
|
||||
|
|
@ -347,14 +356,6 @@ private:
|
|||
|
||||
Data dataForTzTransition(QTzTransitionTime tran) const;
|
||||
Data dataFromRule(QTzTransitionRule rule, qint64 msecsSinceEpoch) const;
|
||||
#if QT_CONFIG(icu)
|
||||
# ifdef __cpp_lib_is_final
|
||||
static_assert(std::is_final<QIcuTimeZonePrivate>::value,
|
||||
"if QIcuTimeZonePrivate isn't final, we may need to specialize "
|
||||
"QExplicitlySharedDataPointer::clone() to call QTimeZonePrivate::clone()");
|
||||
# endif
|
||||
mutable QExplicitlySharedDataPointer<const QIcuTimeZonePrivate> m_icu;
|
||||
#endif
|
||||
QTzTimeZoneCacheEntry cached_data;
|
||||
const QList<QTzTransitionTime> &tranCache() const { return cached_data.m_tranTimes; }
|
||||
};
|
||||
|
|
|
|||
|
|
@ -33,10 +33,6 @@ QT_BEGIN_NAMESPACE
|
|||
|
||||
using namespace Qt::StringLiterals;
|
||||
|
||||
#if QT_CONFIG(icu)
|
||||
Q_CONSTINIT static QBasicMutex s_icu_mutex;
|
||||
#endif
|
||||
|
||||
/*
|
||||
Private
|
||||
|
||||
|
|
@ -771,9 +767,6 @@ QTzTimeZonePrivate::~QTzTimeZonePrivate()
|
|||
|
||||
QTzTimeZonePrivate *QTzTimeZonePrivate::clone() const
|
||||
{
|
||||
#if QT_CONFIG(icu)
|
||||
const auto lock = qt_scoped_lock(s_icu_mutex);
|
||||
#endif
|
||||
return new QTzTimeZonePrivate(*this);
|
||||
}
|
||||
|
||||
|
|
@ -1007,15 +1000,7 @@ QTzTimeZonePrivate::QTzTimeZonePrivate(const QByteArray &ianaId)
|
|||
if (m_id.isEmpty()) {
|
||||
// This can only happen for the system zone, when we've read the
|
||||
// contents of /etc/localtime because it wasn't a symlink.
|
||||
#if QT_CONFIG(icu)
|
||||
// Use ICU's system zone, if only to avoid using the abbreviation as ID
|
||||
// (ICU might mis-recognize it) in displayName().
|
||||
m_icu = new QIcuTimeZonePrivate();
|
||||
// Use its ID, as an alternate source of data:
|
||||
m_id = m_icu->id();
|
||||
if (!m_id.isEmpty())
|
||||
return;
|
||||
#endif
|
||||
// TODO: use CLDR generic abbreviation for the zone.
|
||||
m_id = abbreviation(QDateTime::currentMSecsSinceEpoch()).toUtf8();
|
||||
}
|
||||
}
|
||||
|
|
@ -1034,18 +1019,6 @@ QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|||
QTimeZone::NameType nameType,
|
||||
const QLocale &locale) const
|
||||
{
|
||||
// TZ DB lacks localized names (it only has IANA IDs), so delegate to ICU
|
||||
// for those, when available.
|
||||
#if QT_CONFIG(icu)
|
||||
{
|
||||
auto lock = qt_scoped_lock(s_icu_mutex);
|
||||
// TODO Some valid TZ names are not valid ICU names, use translation table?
|
||||
if (!m_icu)
|
||||
m_icu = new QIcuTimeZonePrivate(m_id);
|
||||
if (m_icu->isValid())
|
||||
return m_icu->displayName(timeType, nameType, locale);
|
||||
}
|
||||
#endif
|
||||
// TZ only provides C-locale abbreviations and offset:
|
||||
if (nameType != QTimeZone::LongName && isDataLocale(locale)) {
|
||||
QTimeZonePrivate::Data tran = data(timeType);
|
||||
|
|
@ -1057,7 +1030,7 @@ QString QTzTimeZonePrivate::displayName(QTimeZone::TimeType timeType,
|
|||
return isoOffsetFormat(tran.offsetFromUtc);
|
||||
}
|
||||
}
|
||||
// Otherwise, fall back to base class:
|
||||
// Otherwise, fall back to base class (and qtimezonelocale.cpp):
|
||||
return QTimeZonePrivate::displayName(timeType, nameType, locale);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1280,7 +1280,7 @@ void tst_QTimeZone::utcTest()
|
|||
|
||||
void tst_QTimeZone::icuTest()
|
||||
{
|
||||
#if defined(QT_BUILD_INTERNAL) && QT_CONFIG(icu)
|
||||
#if defined(QT_BUILD_INTERNAL) && QT_CONFIG(icu) && !defined(Q_OS_UNIX)
|
||||
// Known datetimes
|
||||
qint64 std = QDateTime(QDate(2012, 1, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch();
|
||||
qint64 dst = QDateTime(QDate(2012, 6, 1), QTime(0, 0), QTimeZone::UTC).toMSecsSinceEpoch();
|
||||
|
|
@ -1323,7 +1323,7 @@ void tst_QTimeZone::icuTest()
|
|||
if (QTest::currentTestFailed())
|
||||
return;
|
||||
testEpochTranPrivate(QIcuTimeZonePrivate("America/Toronto"));
|
||||
#endif // icu
|
||||
#endif // ICU not on Unix
|
||||
}
|
||||
|
||||
void tst_QTimeZone::tzTest()
|
||||
|
|
@ -1527,7 +1527,7 @@ void tst_QTimeZone::tzTest()
|
|||
QDateTime dt(QDate(2016, 3, 28), QTime(0, 0), UTC);
|
||||
QCOMPARE(tzBarnaul.data(dt.toMSecsSinceEpoch()).abbreviation, QString("+07"));
|
||||
}
|
||||
#endif // QT_BUILD_INTERNAL && Q_OS_UNIX && !Q_OS_DARWIN
|
||||
#endif // QT_BUILD_INTERNAL && Q_OS_UNIX && !Q_OS_DARWIN && !Q_OS_ANDROID
|
||||
}
|
||||
|
||||
void tst_QTimeZone::macTest()
|
||||
|
|
|
|||
Loading…
Reference in New Issue