OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
scottmg
2014/02/03 23:58:15
nit; just Copyright 2014 since it's new.
| |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 #include "content/browser/gamepad/raw_input_data_fetcher_win.h" | |
6 | |
7 #include "base/debug/trace_event.h" | |
8 //#include "base/win/windows_version.h" | |
scottmg
2014/02/03 23:58:15
nit; remove
| |
9 #include "content/common/gamepad_hardware_buffer.h" | |
10 #include "content/common/gamepad_messages.h" | |
11 | |
12 namespace content { | |
13 | |
14 using namespace blink; | |
15 | |
16 namespace { | |
17 | |
18 float NormalizeAxis(long value, long min, long max) { | |
19 return (2.f * (value - min) / static_cast<float>(max - min)) - 1.f; | |
20 } | |
21 | |
22 // From the HID Usage Tables specification. | |
23 USHORT DeviceUsages[] = { | |
24 0x04, // Joysticks | |
25 0x05, // Gamepads | |
26 0x08, // Multi Axis | |
27 }; | |
28 | |
29 const uint32_t kAxisMinimumUsageNumber = 0x30; | |
30 | |
31 } // namespace | |
32 | |
33 RawInputDataFetcher::RawInputDataFetcher() | |
34 : hid_dll_(base::FilePath(FILE_PATH_LITERAL("hid.dll"))) | |
35 , rawinput_available_(GetHidDllFunctions()) | |
36 , filter_xinput_(true) | |
37 , events_monitored_(false) {} | |
38 | |
39 RawInputDataFetcher::~RawInputDataFetcher() { | |
40 DCHECK(!window_); | |
41 DCHECK(!events_monitored_); | |
42 } | |
43 | |
44 void RawInputDataFetcher::WillDestroyCurrentMessageLoop() { | |
45 StopMonitor(); | |
46 } | |
47 | |
48 RAWINPUTDEVICE* RawInputDataFetcher::GetRawInputDevices(DWORD flags) { | |
49 int usage_count = arraysize(DeviceUsages); | |
50 scoped_ptr<RAWINPUTDEVICE[]> devices(new RAWINPUTDEVICE[usage_count]); | |
51 for (int i = 0; i < usage_count; ++i) { | |
52 devices[i].dwFlags = flags; | |
53 devices[i].usUsagePage = 1; | |
54 devices[i].usUsage = DeviceUsages[i]; | |
55 devices[i].hwndTarget = window_->hwnd(); | |
56 } | |
57 return devices.release(); | |
58 } | |
59 | |
60 void RawInputDataFetcher::StartMonitor() { | |
61 if (!rawinput_available_ || events_monitored_) | |
62 return; | |
63 | |
64 if (!window_) { | |
65 window_.reset(new base::win::MessageWindow()); | |
66 if (!window_->Create(base::Bind(&RawInputDataFetcher::HandleMessage, | |
67 base::Unretained(this)))) { | |
68 LOG_GETLASTERROR(ERROR) << "Failed to create the raw input window"; | |
69 window_.reset(); | |
70 return; | |
71 } | |
72 } | |
73 | |
74 // Register to receive raw HID input. | |
75 scoped_ptr<RAWINPUTDEVICE[]> devices(GetRawInputDevices(RIDEV_INPUTSINK)); | |
76 if (!RegisterRawInputDevices(devices.get(), arraysize(DeviceUsages), | |
77 sizeof(RAWINPUTDEVICE))) { | |
78 LOG_GETLASTERROR(ERROR) | |
79 << "RegisterRawInputDevices() failed for RIDEV_INPUTSINK"; | |
80 window_.reset(); | |
81 return; | |
82 } | |
83 | |
84 // Start observing message loop destruction if we start monitoring the first | |
85 // event. | |
86 if (!events_monitored_) | |
87 base::MessageLoop::current()->AddDestructionObserver(this); | |
88 | |
89 events_monitored_ = true; | |
90 } | |
91 | |
92 void RawInputDataFetcher::StopMonitor() { | |
93 if (!rawinput_available_ || !events_monitored_) | |
94 return; | |
95 | |
96 // Stop receiving raw input. | |
97 DCHECK(window_); | |
98 scoped_ptr<RAWINPUTDEVICE[]> devices(GetRawInputDevices(RIDEV_REMOVE)); | |
99 | |
100 if (!RegisterRawInputDevices(devices.get(), arraysize(DeviceUsages), | |
101 sizeof(RAWINPUTDEVICE))) { | |
102 LOG_GETLASTERROR(INFO) | |
103 << "RegisterRawInputDevices() failed for RIDEV_REMOVE"; | |
104 } | |
105 | |
106 events_monitored_ = false; | |
107 window_.reset(); | |
108 ClearControllers(); | |
109 | |
110 // Stop observing message loop destruction if no event is being monitored. | |
111 base::MessageLoop::current()->RemoveDestructionObserver(this); | |
112 } | |
113 | |
114 void RawInputDataFetcher::ClearControllers() { | |
115 while (!controllers_.empty()) { | |
116 RawGamepadInfo* gamepad_info = controllers_.begin()->second; | |
117 controllers_.erase(gamepad_info->handle); | |
118 delete gamepad_info; | |
119 } | |
120 } | |
121 | |
122 std::vector<RawGamepadInfo*> RawInputDataFetcher::EnumerateDevices() { | |
123 std::vector<RawGamepadInfo*> valid_controllers; | |
124 | |
125 ClearControllers(); | |
126 | |
127 UINT count = 0; | |
128 UINT result = GetRawInputDeviceList(NULL, &count, sizeof(RAWINPUTDEVICELIST)); | |
129 if (result == static_cast<UINT>(-1)) { | |
130 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceList() failed"; | |
131 return valid_controllers; | |
132 } | |
133 DCHECK_EQ(0u, result); | |
134 | |
135 scoped_ptr<RAWINPUTDEVICELIST[]> device_list(new RAWINPUTDEVICELIST[count]); | |
136 result = GetRawInputDeviceList(device_list.get(), &count, | |
137 sizeof(RAWINPUTDEVICELIST)); | |
138 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
another static_cast here
| |
139 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceList() failed"; | |
140 return valid_controllers; | |
141 } | |
142 DCHECK_EQ(count, result); | |
143 | |
144 for (UINT i = 0; i < count; ++i) { | |
145 if (device_list[i].dwType == RIM_TYPEHID) { | |
146 HANDLE device_handle = device_list[i].hDevice; | |
147 RawGamepadInfo* gamepad_info = ParseGamepadInfo(device_handle); | |
148 if (gamepad_info) { | |
149 controllers_[device_handle] = gamepad_info; | |
150 valid_controllers.push_back(gamepad_info); | |
151 } | |
152 } | |
153 } | |
154 return valid_controllers; | |
155 } | |
156 | |
157 RawGamepadInfo* RawInputDataFetcher::GetGamepadInfo(HANDLE handle) { | |
158 std::map<HANDLE, RawGamepadInfo*>::iterator it = controllers_.find(handle); | |
159 if (it != controllers_.end()) | |
160 return it->second; | |
161 | |
162 return NULL; | |
163 } | |
164 | |
165 RawGamepadInfo* RawInputDataFetcher::ParseGamepadInfo(HANDLE hDevice) { | |
166 UINT size = 0; | |
167 | |
168 // Do we already have this device in the map? | |
169 if (GetGamepadInfo(hDevice)) | |
170 return NULL; | |
171 | |
172 // Query basic device info. | |
173 UINT result = GetRawInputDeviceInfo(hDevice, RIDI_DEVICEINFO, | |
174 NULL, &size); | |
175 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
another static_cast here
| |
176 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
177 return NULL; | |
178 } | |
179 DCHECK_EQ(0u, result); | |
180 | |
181 scoped_ptr<uint8[]> di_buffer(new uint8[size]); | |
182 RID_DEVICE_INFO* device_info = | |
183 reinterpret_cast<RID_DEVICE_INFO*>(di_buffer.get()); | |
184 result = GetRawInputDeviceInfo(hDevice, RIDI_DEVICEINFO, | |
185 di_buffer.get(), &size); | |
186 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
another static_cast here
| |
187 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
188 return NULL; | |
189 } | |
190 DCHECK_EQ(size, result); | |
191 | |
192 // Make sure this device is of a type that we want to observe. | |
193 bool valid_type = false; | |
194 for (int i = 0; i < arraysize(DeviceUsages); ++i) { | |
195 if (device_info->hid.usUsage == DeviceUsages[i]) { | |
196 valid_type = true; | |
197 break; | |
198 } | |
199 } | |
200 | |
201 if (!valid_type) | |
202 return NULL; | |
scottmg
2014/02/03 23:58:15
nit; indent wrong (git cl format?)
bajones
2014/02/04 01:00:35
git cl format is broken for me right now for unkno
| |
203 | |
204 scoped_ptr<RawGamepadInfo> gamepad_info(new RawGamepadInfo); | |
205 gamepad_info->handle = hDevice; | |
scottmg
2014/02/03 23:58:15
please reorder the ones near the top here so they
| |
206 gamepad_info->report_id = 0; | |
207 gamepad_info->axes_length = 0; | |
208 gamepad_info->buttons_length = 0; | |
209 | |
210 ZeroMemory(gamepad_info->buttons, sizeof(gamepad_info->buttons)); | |
211 ZeroMemory(gamepad_info->axes, sizeof(gamepad_info->axes)); | |
212 | |
213 gamepad_info->vendor_id = device_info->hid.dwVendorId; | |
214 gamepad_info->product_id = device_info->hid.dwProductId; | |
215 | |
216 // Query device identifier | |
217 result = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, | |
218 NULL, &size); | |
219 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here
| |
220 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
221 return NULL; | |
222 } | |
223 DCHECK_EQ(0u, result); | |
224 | |
225 scoped_ptr<wchar_t[]> name_buffer(new wchar_t[size]); | |
226 result = GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, | |
227 name_buffer.get(), &size); | |
228 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here :/
| |
229 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
230 return NULL; | |
231 } | |
232 DCHECK_EQ(size, result); | |
233 | |
234 // The presence of "IG_" in the device name indicates that this is an XInput | |
235 // Gamepad. Skip enumerating these devices and let the XInput path handle it. | |
236 // http://msdn.microsoft.com/en-us/library/windows/desktop/ee417014.aspx | |
237 if (filter_xinput_ && wcsstr( name_buffer.get(), L"IG_" ) ) { | |
238 return NULL; | |
239 } | |
240 | |
241 // Get a friendly device name | |
242 BOOLEAN got_product_string = FALSE; | |
243 HANDLE hid_handle = CreateFile(name_buffer.get(), GENERIC_READ|GENERIC_WRITE, | |
244 FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING, NULL, NULL); | |
245 if (hid_handle) { | |
246 got_product_string = hidd_get_product_string_(hid_handle, gamepad_info->id, | |
247 sizeof(gamepad_info->id)); | |
248 CloseHandle(hid_handle); | |
249 } | |
250 | |
251 if (!got_product_string) | |
252 swprintf(gamepad_info->id, WebGamepad::idLengthCap, L"Unknown Gamepad"); | |
scottmg
2014/02/03 23:58:15
is "Gamepad" correct? i.e. DeviceUsages name inste
bajones
2014/02/04 01:00:35
Well then, ignored it is! :)
(Might squeeze this i
| |
253 | |
254 // Query device capabilities. | |
255 result = GetRawInputDeviceInfo(hDevice, RIDI_PREPARSEDDATA, | |
256 NULL, &size); | |
257 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here
| |
258 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
259 return NULL; | |
260 } | |
261 DCHECK_EQ(0u, result); | |
262 | |
263 gamepad_info->ppd_buffer.reset(new uint8[size]); | |
264 gamepad_info->preparsed_data = | |
265 reinterpret_cast<PHIDP_PREPARSED_DATA>(gamepad_info->ppd_buffer.get()); | |
266 result = GetRawInputDeviceInfo(hDevice, RIDI_PREPARSEDDATA, | |
267 gamepad_info->ppd_buffer.get(), &size); | |
268 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here
| |
269 LOG_GETLASTERROR(ERROR) << "GetRawInputDeviceInfo() failed"; | |
270 return NULL; | |
271 } | |
272 DCHECK_EQ(size, result); | |
273 | |
274 HIDP_CAPS caps; | |
275 NTSTATUS status = hidp_get_caps_(gamepad_info->preparsed_data, &caps); | |
276 DCHECK_EQ(HIDP_STATUS_SUCCESS, status); | |
277 | |
278 // Query button information. | |
279 gamepad_info->button_caps_length = caps.NumberInputButtonCaps; | |
280 USHORT count = gamepad_info->button_caps_length; | |
281 if (count > 0) { | |
282 gamepad_info->button_caps.reset(new HIDP_BUTTON_CAPS[count]); | |
283 status = hidp_get_button_caps_(HidP_Input, gamepad_info->button_caps.get(), | |
284 &count, gamepad_info->preparsed_data); | |
285 DCHECK_EQ(HIDP_STATUS_SUCCESS, status); | |
286 | |
287 uint32_t min_index, max_index; | |
288 for (uint32_t i = 0; i < count; ++i) { | |
289 min_index = gamepad_info->button_caps[i].Range.UsageMin - 1; | |
scottmg
2014/02/03 23:58:15
can UsageMin be 0?
bajones
2014/02/04 01:00:35
If the device is well behaved? No. Should we rely
| |
290 if (min_index < WebGamepad::buttonsLengthCap) { | |
291 max_index = std::min( | |
292 static_cast<size_t>(gamepad_info->button_caps[i].Range.UsageMax), | |
293 WebGamepad::buttonsLengthCap); | |
294 gamepad_info->buttons_length = std::max( | |
295 gamepad_info->buttons_length, max_index); | |
296 } | |
297 } | |
298 } | |
299 | |
300 // Query axis information. | |
301 count = caps.NumberInputValueCaps; | |
302 scoped_ptr<HIDP_VALUE_CAPS[]> axes_caps(new HIDP_VALUE_CAPS[count]); | |
303 status = hidp_get_value_caps_(HidP_Input, axes_caps.get(), &count, | |
304 gamepad_info->preparsed_data); | |
305 | |
306 for (UINT i = 0; i < count; i++) { | |
307 uint32_t axis_index = axes_caps[i].Range.UsageMin - kAxisMinimumUsageNumber; | |
308 if (axis_index < WebGamepad::axesLengthCap) { | |
309 gamepad_info->axes[axis_index].caps = axes_caps[i]; | |
310 gamepad_info->axes[axis_index].value = 0; | |
311 gamepad_info->axes_length = | |
312 std::max(gamepad_info->axes_length, axis_index + 1); | |
313 } | |
314 } | |
315 | |
316 return gamepad_info.release(); | |
317 } | |
318 | |
319 void RawInputDataFetcher::UpdateGamepad( | |
320 RAWINPUT* input, | |
321 RawGamepadInfo* gamepad_info) { | |
322 NTSTATUS status; | |
323 | |
324 gamepad_info->report_id++; | |
325 | |
326 // Query button state. | |
327 if (gamepad_info->buttons_length) { | |
328 uint32_t min_index, max_index; | |
329 ULONG button_index; | |
330 for (uint32_t i = 0; i < gamepad_info->button_caps_length; ++i) { | |
331 HIDP_BUTTON_CAPS* button_caps = &gamepad_info->button_caps[i]; | |
332 min_index = button_caps->Range.UsageMin; | |
333 if (min_index <= WebGamepad::buttonsLengthCap) { | |
334 ULONG buttons_length = button_caps->Range.UsageMax - | |
335 button_caps->Range.UsageMin + 1; | |
336 scoped_ptr<USAGE[]> usages(new USAGE[buttons_length]); | |
scottmg
2014/02/03 23:58:15
These new's make me (pointlessly) cringe in the Up
| |
337 status = hidp_get_usages_(HidP_Input, | |
338 button_caps->UsagePage, 0, usages.get(), &buttons_length, | |
339 gamepad_info->preparsed_data, | |
340 reinterpret_cast<PCHAR>(input->data.hid.bRawData), | |
341 input->data.hid.dwSizeHid); | |
342 | |
343 if (status == HIDP_STATUS_SUCCESS) { | |
344 // Clear this range of buttons. | |
345 max_index = std::min( | |
346 static_cast<size_t>(button_caps->Range.UsageMax), | |
347 WebGamepad::buttonsLengthCap); | |
348 ZeroMemory(&gamepad_info->buttons[min_index-1], | |
349 sizeof(bool) * (max_index - min_index + 1)); | |
350 | |
351 // Set each reported button to true. | |
352 for (uint32_t j = 0; j < buttons_length; j++) { | |
353 button_index = usages[j] - 1; | |
354 if (button_index < blink::WebGamepad::buttonsLengthCap) | |
355 gamepad_info->buttons[button_index] = true; | |
356 } | |
357 } | |
358 } | |
359 } | |
360 } | |
361 | |
362 // Query axis state. | |
363 ULONG axis_value = 0; | |
364 LONG scaled_axis_value = 0; | |
365 for (uint32_t i = 0; i < gamepad_info->axes_length; i++) { | |
366 RawGamepadAxis* axis = &gamepad_info->axes[i]; | |
367 | |
368 // If the min in < 0 we have to query the scaled value, otherwise we need | |
scottmg
2014/02/03 23:58:15
in -> is
| |
369 // the normal unscaled value. | |
370 if (axis->caps.LogicalMin < 0) { | |
371 status = hidp_get_scaled_usage_value_(HidP_Input, axis->caps.UsagePage, 0, | |
372 axis->caps.Range.UsageMin, &scaled_axis_value, | |
373 gamepad_info->preparsed_data, | |
374 reinterpret_cast<PCHAR>(input->data.hid.bRawData), | |
375 input->data.hid.dwSizeHid); | |
376 if (status == HIDP_STATUS_SUCCESS) { | |
377 axis->value = NormalizeAxis(scaled_axis_value, | |
378 axis->caps.LogicalMin, axis->caps.LogicalMax); | |
379 } | |
380 } else { | |
381 status = hidp_get_usage_value_(HidP_Input, axis->caps.UsagePage, 0, | |
382 axis->caps.Range.UsageMin, &axis_value, | |
383 gamepad_info->preparsed_data, | |
384 reinterpret_cast<PCHAR>(input->data.hid.bRawData), | |
385 input->data.hid.dwSizeHid); | |
386 if (status == HIDP_STATUS_SUCCESS) { | |
387 axis->value = NormalizeAxis(axis_value, | |
388 axis->caps.LogicalMin, axis->caps.LogicalMax); | |
389 } | |
390 } | |
391 } | |
392 } | |
393 | |
394 LRESULT RawInputDataFetcher::OnInput(HRAWINPUT input_handle) { | |
395 // Get the size of the input record. | |
396 UINT size = 0; | |
397 UINT result = GetRawInputData( | |
398 input_handle, RID_INPUT, NULL, &size, sizeof(RAWINPUTHEADER)); | |
399 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here
| |
400 LOG_GETLASTERROR(ERROR) << "GetRawInputData() failed"; | |
401 return 0; | |
402 } | |
403 DCHECK_EQ(0u, result); | |
404 | |
405 // Retrieve the input record. | |
406 scoped_ptr<uint8[]> buffer(new uint8[size]); | |
407 RAWINPUT* input = reinterpret_cast<RAWINPUT*>(buffer.get()); | |
408 result = GetRawInputData( | |
409 input_handle, RID_INPUT, buffer.get(), &size, sizeof(RAWINPUTHEADER)); | |
410 if (result == -1) { | |
scottmg
2014/02/03 23:58:15
and here
| |
411 LOG_GETLASTERROR(ERROR) << "GetRawInputData() failed"; | |
412 return 0; | |
413 } | |
414 DCHECK_EQ(size, result); | |
415 | |
416 // Notify the observer about events generated locally. | |
417 if (input->header.dwType == RIM_TYPEHID && input->header.hDevice != NULL) { | |
418 RawGamepadInfo* gamepad = GetGamepadInfo(input->header.hDevice); | |
419 if (gamepad) | |
420 UpdateGamepad(input, gamepad); | |
421 } | |
422 | |
423 return DefRawInputProc(&input, 1, sizeof(RAWINPUTHEADER)); | |
scottmg
2014/02/03 23:58:15
do we want to pass these on to the DefProc? it see
bajones
2014/02/04 01:00:35
MSDN says that we should be calling it every time
scottmg
2014/02/04 01:15:06
Sorry, I don't see that there? I see DefWindowProc
bajones
2014/02/04 03:53:45
Sorry, I misread DefWindowProc as DefRawInputProc
| |
424 } | |
425 | |
426 bool RawInputDataFetcher::HandleMessage(UINT message, | |
427 WPARAM wparam, | |
428 LPARAM lparam, | |
429 LRESULT* result) { | |
430 switch (message) { | |
431 case WM_INPUT: | |
432 *result = OnInput(reinterpret_cast<HRAWINPUT>(lparam)); | |
433 return true; | |
434 | |
435 default: | |
436 return false; | |
437 } | |
438 } | |
439 | |
440 bool RawInputDataFetcher::GetHidDllFunctions() { | |
441 hidp_get_caps_ = NULL; | |
442 hidp_get_button_caps_ = NULL; | |
443 hidp_get_value_caps_ = NULL; | |
444 hidp_get_usages_ = NULL; | |
445 hidp_get_usage_value_ = NULL; | |
446 hidp_get_scaled_usage_value_ = NULL; | |
447 hidd_get_product_string_ = NULL; | |
448 | |
449 if (!hid_dll_.is_valid()) return false; | |
450 | |
451 hidp_get_caps_ = reinterpret_cast<HidPGetCapsFunc>( | |
452 hid_dll_.GetFunctionPointer("HidP_GetCaps")); | |
453 if (!hidp_get_caps_) | |
454 return false; | |
455 hidp_get_button_caps_ = reinterpret_cast<HidPGetButtonCapsFunc>( | |
456 hid_dll_.GetFunctionPointer("HidP_GetButtonCaps")); | |
457 if (!hidp_get_button_caps_) | |
458 return false; | |
459 hidp_get_value_caps_ = reinterpret_cast<HidPGetValueCapsFunc>( | |
460 hid_dll_.GetFunctionPointer("HidP_GetValueCaps")); | |
461 if (!hidp_get_value_caps_) | |
462 return false; | |
463 hidp_get_usages_ = reinterpret_cast<HidPGetUsagesFunc>( | |
464 hid_dll_.GetFunctionPointer("HidP_GetUsages")); | |
465 if (!hidp_get_usages_) | |
466 return false; | |
467 hidp_get_usage_value_ = reinterpret_cast<HidPGetUsageValueFunc>( | |
468 hid_dll_.GetFunctionPointer("HidP_GetUsageValue")); | |
469 if (!hidp_get_usage_value_) | |
470 return false; | |
471 hidp_get_scaled_usage_value_ = reinterpret_cast<HidPGetScaledUsageValueFunc>( | |
472 hid_dll_.GetFunctionPointer("HidP_GetScaledUsageValue")); | |
473 if (!hidp_get_scaled_usage_value_) | |
474 return false; | |
475 hidd_get_product_string_ = reinterpret_cast<HidDGetStringFunc>( | |
476 hid_dll_.GetFunctionPointer("HidD_GetProductString")); | |
477 if (!hidd_get_product_string_) | |
478 return false; | |
479 | |
480 return true; | |
481 } | |
482 | |
483 } // namespace content | |
OLD | NEW |