Commit bf1b96ef authored by Tibor Goldschwendt's avatar Tibor Goldschwendt Committed by Commit Bot

[android] Init ICU with extra data if extra ICU module is installed

The purpose of this CL is to provide ICU support for 30 extra languages
(see crrev/c/1648623 for more details). crrev/c/1811973 adds a feature
module that delivers an extra ICU data file containing support for the
aforementioned extra languages. To make use of this extra data file, we
check at browser process startup whether the extra ICU feature module is
installed and if so init ICU with the extra data file inside the module.
We retrieve the module install state from the ContentBrowserClient as
feature modules are a chrome layer concept and the ICU initialization
happens in the content layer. The extra ICU data file is applied
_before_ the main ICU data file. Otherwise, the main ICU data file would
override the extra languages.

Requires crrev/c/1834464.

Bug: 1006794
Change-Id: Ic7c14bf3437309b90cf2ce205ade14da937203c0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1818876
Commit-Queue: Tibor Goldschwendt <tiborg@chromium.org>
Reviewed-by: default avatarBo <boliu@chromium.org>
Reviewed-by: default avatarTed Choc <tedchoc@chromium.org>
Cr-Commit-Position: refs/heads/master@{#703491}
parent 70f997f7
......@@ -34,6 +34,7 @@ import("//build/nocompile.gni")
import("//build/timestamp.gni")
import("//testing/libfuzzer/fuzzer_test.gni")
import("//testing/test.gni")
import("//third_party/icu/config.gni")
if (is_mac) {
# Used to generate fuzzer corpus :base_mach_port_rendezvous_convert_corpus.
......@@ -2549,6 +2550,7 @@ test("base_unittests") {
"i18n/character_encoding_unittest.cc",
"i18n/file_util_icu_unittest.cc",
"i18n/icu_string_conversions_unittest.cc",
"i18n/icu_util_unittest.cc",
"i18n/message_formatter_unittest.cc",
"i18n/number_formatting_unittest.cc",
"i18n/rtl_unittest.cc",
......@@ -2929,6 +2931,15 @@ test("base_unittests") {
]
}
if (icu_use_data_file) {
if (is_android) {
deps += [ "//third_party/icu:icu_extra_assets" ]
} else {
deps += [ "//third_party/icu:extra_icudata" ]
data += [ "$root_out_dir/icudtl_extra.dat" ]
}
}
if (is_ios) {
# ios does not use test_launcher to run gtests.
sources -= [
......
......@@ -69,9 +69,11 @@ wchar_t g_debug_icu_pf_filename[_MAX_PATH];
// build pkg configurations, etc). 'l' stands for Little Endian.
// This variable is exported through the header file.
const char kIcuDataFileName[] = "icudtl.dat";
const char kIcuExtraDataFileName[] = "icudtl_extra.dat";
#if defined(OS_ANDROID)
const char kAndroidAssetsIcuDataFileName[] = "assets/icudtl.dat";
#endif
const char kAssetsPathPrefix[] = "assets/";
#endif // defined(OS_ANDROID)
// File handle intentionally never closed. Not using File here because its
// Windows implementation guards against two instances owning the same
......@@ -79,25 +81,31 @@ const char kAndroidAssetsIcuDataFileName[] = "assets/icudtl.dat";
PlatformFile g_icudtl_pf = kInvalidPlatformFile;
MemoryMappedFile* g_icudtl_mapped_file = nullptr;
MemoryMappedFile::Region g_icudtl_region;
void LazyInitIcuDataFile() {
if (g_icudtl_pf != kInvalidPlatformFile) {
return;
}
PlatformFile g_icudtl_extra_pf = kInvalidPlatformFile;
MemoryMappedFile* g_icudtl_extra_mapped_file = nullptr;
MemoryMappedFile::Region g_icudtl_extra_region;
struct PfRegion {
public:
PlatformFile pf;
MemoryMappedFile::Region region;
};
std::unique_ptr<PfRegion> OpenIcuDataFile(const std::string& filename) {
auto result = std::make_unique<PfRegion>();
#if defined(OS_ANDROID)
int fd =
android::OpenApkAsset(kAndroidAssetsIcuDataFileName, &g_icudtl_region);
g_icudtl_pf = fd;
if (fd != -1) {
return;
result->pf =
android::OpenApkAsset(kAssetsPathPrefix + filename, &result->region);
if (result->pf != -1) {
return result;
}
// For unit tests, data file is located on disk, so try there as a fallback.
#endif // defined(OS_ANDROID)
// For unit tests, data file is located on disk, so try there as a fallback.
#if !defined(OS_MACOSX)
FilePath data_path;
if (!PathService::Get(DIR_ASSETS, &data_path)) {
LOG(ERROR) << "Can't find " << kIcuDataFileName;
return;
LOG(ERROR) << "Can't find " << filename;
return nullptr;
}
#if defined(OS_WIN)
// TODO(brucedawson): http://crbug.com/445616
......@@ -105,7 +113,7 @@ void LazyInitIcuDataFile() {
wcscpy_s(tmp_buffer, as_wcstr(data_path.value()));
debug::Alias(tmp_buffer);
#endif
data_path = data_path.AppendASCII(kIcuDataFileName);
data_path = data_path.AppendASCII(filename);
#if defined(OS_WIN)
// TODO(brucedawson): http://crbug.com/445616
......@@ -116,8 +124,7 @@ void LazyInitIcuDataFile() {
#else // !defined(OS_MACOSX)
// Assume it is in the framework bundle's Resources directory.
ScopedCFTypeRef<CFStringRef> data_file_name(
SysUTF8ToCFStringRef(kIcuDataFileName));
ScopedCFTypeRef<CFStringRef> data_file_name(SysUTF8ToCFStringRef(filename));
FilePath data_path = mac::PathForFrameworkBundleResource(data_file_name);
#if defined(OS_IOS)
FilePath override_data_path = ios::FilePathOfEmbeddedICU();
......@@ -126,8 +133,8 @@ void LazyInitIcuDataFile() {
}
#endif // !defined(OS_IOS)
if (data_path.empty()) {
LOG(ERROR) << kIcuDataFileName << " not found in bundle";
return;
LOG(ERROR) << filename << " not found in bundle";
return nullptr;
}
#endif // !defined(OS_MACOSX)
File file(data_path, File::FLAG_OPEN | File::FLAG_READ);
......@@ -139,8 +146,8 @@ void LazyInitIcuDataFile() {
g_debug_icu_pf_filename[0] = 0;
#endif // OS_WIN
g_icudtl_pf = file.TakePlatformFile();
g_icudtl_region = MemoryMappedFile::Region::kWholeFile;
result->pf = file.TakePlatformFile();
result->region = MemoryMappedFile::Region::kWholeFile;
}
#if defined(OS_WIN)
else {
......@@ -150,6 +157,47 @@ void LazyInitIcuDataFile() {
wcscpy_s(g_debug_icu_pf_filename, as_wcstr(data_path.value()));
}
#endif // OS_WIN
return result;
}
void LazyOpenIcuDataFile() {
if (g_icudtl_pf != kInvalidPlatformFile) {
return;
}
auto pf_region = OpenIcuDataFile(kIcuDataFileName);
if (!pf_region) {
return;
}
g_icudtl_pf = pf_region->pf;
g_icudtl_region = pf_region->region;
}
int LoadIcuData(PlatformFile data_fd,
const MemoryMappedFile::Region& data_region,
std::unique_ptr<MemoryMappedFile>* out_mapped_data_file,
UErrorCode* out_error_code) {
if (data_fd == kInvalidPlatformFile) {
LOG(ERROR) << "Invalid file descriptor to ICU data received.";
return 1; // To debug http://crbug.com/445616.
}
out_mapped_data_file->reset(new MemoryMappedFile());
if (!(*out_mapped_data_file)->Initialize(File(data_fd), data_region)) {
LOG(ERROR) << "Couldn't mmap icu data file";
return 2; // To debug http://crbug.com/445616.
}
(*out_error_code) = U_ZERO_ERROR;
udata_setCommonData(const_cast<uint8_t*>((*out_mapped_data_file)->data()),
out_error_code);
if (U_FAILURE(*out_error_code)) {
LOG(ERROR) << "Failed to initialize ICU with data file: "
<< u_errorName(*out_error_code);
return 3; // To debug http://crbug.com/445616.
}
return 0;
}
bool InitializeICUWithFileDescriptorInternal(
......@@ -160,28 +208,20 @@ bool InitializeICUWithFileDescriptorInternal(
g_debug_icu_load = 0; // To debug http://crbug.com/445616.
return true;
}
if (data_fd == kInvalidPlatformFile) {
g_debug_icu_load = 1; // To debug http://crbug.com/445616.
LOG(ERROR) << "Invalid file descriptor to ICU data received.";
return false;
}
std::unique_ptr<MemoryMappedFile> icudtl_mapped_file(new MemoryMappedFile());
if (!icudtl_mapped_file->Initialize(File(data_fd), data_region)) {
g_debug_icu_load = 2; // To debug http://crbug.com/445616.
LOG(ERROR) << "Couldn't mmap icu data file";
std::unique_ptr<MemoryMappedFile> mapped_file;
UErrorCode err;
g_debug_icu_load = LoadIcuData(data_fd, data_region, &mapped_file, &err);
if (g_debug_icu_load == 1 || g_debug_icu_load == 2) {
return false;
}
g_icudtl_mapped_file = icudtl_mapped_file.release();
g_icudtl_mapped_file = mapped_file.release();
UErrorCode err = U_ZERO_ERROR;
udata_setCommonData(const_cast<uint8_t*>(g_icudtl_mapped_file->data()), &err);
if (err != U_ZERO_ERROR) {
g_debug_icu_load = 3; // To debug http://crbug.com/445616.
if (g_debug_icu_load == 3) {
g_debug_icu_last_error = err;
}
#if defined(OS_ANDROID)
else {
else if (g_debug_icu_load == 0) {
// On Android, we can't leave it up to ICU to set the default timezone
// because ICU's timezone detection does not work in many timezones (e.g.
// Australia/Sydney, Asia/Seoul, Europe/Paris ). Use JNI to detect the host
......@@ -195,7 +235,7 @@ bool InitializeICUWithFileDescriptorInternal(
#endif
// Never try to load ICU data from files.
udata_setFileAccess(UDATA_ONLY_PACKAGES, &err);
return err == U_ZERO_ERROR;
return U_SUCCESS(err);
}
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#endif // !defined(OS_NACL)
......@@ -204,7 +244,23 @@ bool InitializeICUWithFileDescriptorInternal(
#if !defined(OS_NACL)
#if ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#if defined(OS_ANDROID)
bool InitializeExtraICUWithFileDescriptor(
PlatformFile data_fd,
const MemoryMappedFile::Region& data_region) {
if (g_icudtl_pf != kInvalidPlatformFile) {
// Must call InitializeExtraICUWithFileDescriptor() before
// InitializeICUWithFileDescriptor().
return false;
}
std::unique_ptr<MemoryMappedFile> mapped_file;
UErrorCode err;
if (LoadIcuData(data_fd, data_region, &mapped_file, &err) != 0) {
return false;
}
g_icudtl_extra_mapped_file = mapped_file.release();
return true;
}
bool InitializeICUWithFileDescriptor(
PlatformFile data_fd,
const MemoryMappedFile::Region& data_region) {
......@@ -220,7 +276,14 @@ PlatformFile GetIcuDataFileHandle(MemoryMappedFile::Region* out_region) {
*out_region = g_icudtl_region;
return g_icudtl_pf;
}
#endif
PlatformFile GetIcuExtraDataFileHandle(MemoryMappedFile::Region* out_region) {
if (g_icudtl_extra_pf == kInvalidPlatformFile) {
return kInvalidPlatformFile;
}
*out_region = g_icudtl_extra_region;
return g_icudtl_extra_pf;
}
const uint8_t* GetRawIcuMemory() {
CHECK(g_icudtl_mapped_file);
......@@ -244,7 +307,28 @@ bool InitializeICUFromRawMemory(const uint8_t* raw_memory) {
#endif
}
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
bool InitializeExtraICU() {
if (g_icudtl_pf != kInvalidPlatformFile) {
// Must call InitializeExtraICU() before InitializeICU().
return false;
}
auto pf_region = OpenIcuDataFile(kIcuExtraDataFileName);
if (!pf_region) {
return false;
}
g_icudtl_extra_pf = pf_region->pf;
g_icudtl_extra_region = pf_region->region;
std::unique_ptr<MemoryMappedFile> mapped_file;
UErrorCode err;
if (LoadIcuData(g_icudtl_extra_pf, g_icudtl_extra_region, &mapped_file,
&err) != 0) {
return false;
}
g_icudtl_extra_mapped_file = mapped_file.release();
return true;
}
#endif // (ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE)
bool InitializeICU() {
#if DCHECK_IS_ON()
......@@ -261,7 +345,7 @@ bool InitializeICU() {
// it is needed. This can fail if the process is sandboxed at that time.
// Instead, we map the file in and hand off the data so the sandbox won't
// cause any problems.
LazyInitIcuDataFile();
LazyOpenIcuDataFile();
result =
InitializeICUWithFileDescriptorInternal(g_icudtl_pf, g_icudtl_region);
#if defined(OS_WIN)
......@@ -299,5 +383,16 @@ void AllowMultipleInitializeCallsForTesting() {
#endif
}
#if !defined(OS_NACL)
#if ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
void ResetGlobalsForTesting() {
g_icudtl_pf = kInvalidPlatformFile;
g_icudtl_mapped_file = nullptr;
g_icudtl_extra_pf = kInvalidPlatformFile;
g_icudtl_extra_mapped_file = nullptr;
}
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#endif // !defined(OS_NACL)
} // namespace i18n
} // namespace base
......@@ -23,18 +23,28 @@ namespace i18n {
BASE_I18N_EXPORT bool InitializeICU();
#if ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#if defined(OS_ANDROID)
// Returns the PlatformFile and Region that was initialized by InitializeICU().
// Use with InitializeICUWithFileDescriptor().
// Loads ICU's extra data tables from disk for the current process. If used must
// be called before InitializeICU().
BASE_I18N_EXPORT bool InitializeExtraICU();
// Returns the PlatformFile and Region that was initialized by InitializeICU()
// or InitializeExtraICU(). Use with InitializeICUWithFileDescriptor() or
// InitializeExtraICUWithFileDescriptor().
BASE_I18N_EXPORT PlatformFile GetIcuDataFileHandle(
MemoryMappedFile::Region* out_region);
BASE_I18N_EXPORT PlatformFile
GetIcuExtraDataFileHandle(MemoryMappedFile::Region* out_region);
// Android uses a file descriptor passed by browser process to initialize ICU
// in render processes.
// Loads ICU extra data file from file descriptor passed by browser process to
// initialize ICU in render processes. If used must be called before
// InitializeICUWithFileDescriptor().
BASE_I18N_EXPORT bool InitializeExtraICUWithFileDescriptor(
PlatformFile data_fd,
const MemoryMappedFile::Region& data_region);
// Loads ICU data file from file descriptor passed by browser process to
// initialize ICU in render processes.
BASE_I18N_EXPORT bool InitializeICUWithFileDescriptor(
PlatformFile data_fd,
const MemoryMappedFile::Region& data_region);
#endif
// Returns a void pointer to the memory mapped ICU data file.
//
......@@ -60,6 +70,12 @@ BASE_I18N_EXPORT bool InitializeICUFromRawMemory(const uint8_t* raw_memory);
// In a test binary, the call above might occur twice.
BASE_I18N_EXPORT void AllowMultipleInitializeCallsForTesting();
#if !defined(OS_NACL)
#if ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
BASE_I18N_EXPORT void ResetGlobalsForTesting();
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#endif // !defined(OS_NACL)
} // namespace i18n
} // namespace base
......
// Copyright (c) 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/i18n/icu_util.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"
#if !defined(OS_NACL)
#if ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
namespace base {
namespace i18n {
class IcuUtilTest : public testing::Test {
protected:
void SetUp() override { ResetGlobalsForTesting(); }
};
#if defined(OS_ANDROID)
TEST_F(IcuUtilTest, InitializeIcuSucceeds) {
bool success = InitializeICU();
ASSERT_TRUE(success);
}
TEST_F(IcuUtilTest, ExtraFileNotInitializedAtStart) {
MemoryMappedFile::Region region;
PlatformFile file = GetIcuExtraDataFileHandle(&region);
ASSERT_EQ(file, kInvalidPlatformFile);
}
TEST_F(IcuUtilTest, InitializeExtraIcuSucceeds) {
bool success = InitializeExtraICU();
ASSERT_TRUE(success);
}
TEST_F(IcuUtilTest, CannotInitializeExtraIcuAfterIcu) {
InitializeICU();
bool success = InitializeExtraICU();
ASSERT_FALSE(success);
}
TEST_F(IcuUtilTest, ExtraFileInitializedAfterInit) {
InitializeExtraICU();
MemoryMappedFile::Region region;
PlatformFile file = GetIcuExtraDataFileHandle(&region);
ASSERT_NE(file, kInvalidPlatformFile);
}
TEST_F(IcuUtilTest, InitializeExtraIcuFromFdSucceeds) {
InitializeExtraICU();
MemoryMappedFile::Region region;
PlatformFile pf = GetIcuExtraDataFileHandle(&region);
bool success = InitializeExtraICUWithFileDescriptor(pf, region);
ASSERT_TRUE(success);
}
TEST_F(IcuUtilTest, CannotInitializeExtraIcuFromFdAfterIcu) {
InitializeExtraICU();
InitializeICU();
MemoryMappedFile::Region region;
PlatformFile pf = GetIcuExtraDataFileHandle(&region);
bool success = InitializeExtraICUWithFileDescriptor(pf, region);
ASSERT_FALSE(success);
}
#endif // defined(OS_ANDROID)
} // namespace i18n
} // namespace base
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
#endif // !defined(OS_NACL)
......@@ -7,7 +7,7 @@ extra_icu_module_desc = {
android_manifest =
"//chrome/android/modules/extra_icu/internal/java/AndroidManifest.xml"
java_deps = [
"//chrome/android/modules/extra_icu/internal:icudtl_extra_assets",
"//third_party/icu:icu_extra_assets",
"//chrome/android/modules/extra_icu/internal:java",
]
}
......@@ -12,10 +12,3 @@ android_library("java") {
"//chrome/android/modules/extra_icu/public:java",
]
}
android_assets("icudtl_extra_assets") {
sources = [
"//third_party/icu/android_small/icudtl_extra.dat",
]
disable_compression = true
}
......@@ -405,6 +405,7 @@
#elif defined(OS_ANDROID)
#include "base/android/application_status_listener.h"
#include "chrome/android/features/dev_ui/buildflags.h"
#include "chrome/android/modules/extra_icu/provider/module_provider.h"
#include "chrome/browser/android/app_hooks.h"
#include "chrome/browser/android/chrome_context_util.h"
#include "chrome/browser/android/devtools_manager_delegate_android.h"
......@@ -5668,3 +5669,10 @@ void ChromeContentBrowserClient::BlockBluetoothScanning(
CONTENT_SETTINGS_TYPE_BLUETOOTH_SCANNING, std::string(),
CONTENT_SETTING_BLOCK);
}
bool ChromeContentBrowserClient::ShouldLoadExtraIcuDataFile() {
#if defined(OS_ANDROID)
return extra_icu::ModuleProvider::IsModuleInstalled();
#endif
return false;
}
......@@ -614,6 +614,8 @@ class ChromeContentBrowserClient : public content::ContentBrowserClient {
const url::Origin& requesting_origin,
const url::Origin& embedding_origin) override;
bool ShouldLoadExtraIcuDataFile() override;
content::PreviewsState DetermineAllowedPreviewsWithoutHoldback(
content::PreviewsState initial_state,
content::NavigationHandle* navigation_handle,
......
......@@ -764,15 +764,41 @@ int ContentMainRunnerImpl::Initialize(const ContentMainParams& params) {
RegisterContentSchemes(delegate_->ShouldLockSchemeRegistry());
#if defined(OS_ANDROID) && (ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE)
int icudata_fd = g_fds->MaybeGet(kAndroidICUDataDescriptor);
if (icudata_fd != -1) {
auto icudata_region = g_fds->GetRegion(kAndroidICUDataDescriptor);
if (!base::i18n::InitializeICUWithFileDescriptor(icudata_fd,
icudata_region))
// On Android, we have two ICU data files. A main one with most languages
// that is expected to always be available and an extra one that is
// installed separately via a dynamic feature module. If the extra ICU data
// file is available we have to apply it _before_ the main ICU data file.
// Otherwise, the languages of the extra ICU file will be overridden.
if (process_type.empty()) {
// In browser process load ICU data files from disk.
if (GetContentClient()->browser()->ShouldLoadExtraIcuDataFile()) {
if (!base::i18n::InitializeExtraICU()) {
return TerminateForFatalInitializationError();
}
}
if (!base::i18n::InitializeICU()) {
return TerminateForFatalInitializationError();
}
} else {
if (!base::i18n::InitializeICU())
// In child process map ICU data files loaded by browser process.
int icu_extra_data_fd = g_fds->MaybeGet(kAndroidICUExtraDataDescriptor);
if (icu_extra_data_fd != -1) {
auto icu_extra_data_region =
g_fds->GetRegion(kAndroidICUExtraDataDescriptor);
if (!base::i18n::InitializeExtraICUWithFileDescriptor(
icu_extra_data_fd, icu_extra_data_region)) {
return TerminateForFatalInitializationError();
}
}
int icu_data_fd = g_fds->MaybeGet(kAndroidICUDataDescriptor);
if (icu_data_fd == -1) {
return TerminateForFatalInitializationError();
}
auto icu_data_region = g_fds->GetRegion(kAndroidICUDataDescriptor);
if (!base::i18n::InitializeICUWithFileDescriptor(icu_data_fd,
icu_data_region)) {
return TerminateForFatalInitializationError();
}
}
#else
if (!base::i18n::InitializeICU())
......
......@@ -83,6 +83,12 @@ ChildProcessLauncherHelper::GetFilesToMap() {
base::MemoryMappedFile::Region icu_region;
int fd = base::i18n::GetIcuDataFileHandle(&icu_region);
files_to_register->ShareWithRegion(kAndroidICUDataDescriptor, fd, icu_region);
base::MemoryMappedFile::Region icu_extra_region;
int extra_fd = base::i18n::GetIcuExtraDataFileHandle(&icu_extra_region);
if (extra_fd != -1) {
files_to_register->ShareWithRegion(kAndroidICUExtraDataDescriptor, extra_fd,
icu_extra_region);
}
#endif // ICU_UTIL_DATA_IMPL == ICU_UTIL_DATA_FILE
return files_to_register;
......
......@@ -1007,4 +1007,8 @@ void ContentBrowserClient::BlockBluetoothScanning(
const url::Origin& requesting_origin,
const url::Origin& embedding_origin) {}
bool ContentBrowserClient::ShouldLoadExtraIcuDataFile() {
return false;
}
} // namespace content
......@@ -1682,6 +1682,10 @@ class CONTENT_EXPORT ContentBrowserClient {
virtual void BlockBluetoothScanning(content::BrowserContext* browser_context,
const url::Origin& requesting_origin,
const url::Origin& embedding_origin);
// Returns true if the extra ICU data file is available and should be used to
// initialize ICU.
virtual bool ShouldLoadExtraIcuDataFile();
};
} // namespace content
......
......@@ -15,6 +15,7 @@ enum {
#if defined(OS_ANDROID)
kAndroidPropertyDescriptor = service_manager::kFirstEmbedderDescriptor,
kAndroidICUDataDescriptor,
kAndroidICUExtraDataDescriptor,
#endif
// Reserves 100 to 199 for dynamically generated IDs.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment