Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 // Copyright 2014 The Chromium Authors. All rights reserved. | 1 // Copyright 2014 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 #include "device/hid/hid_service_win.h" | 5 #include "device/hid/hid_service_win.h" |
| 6 | 6 |
| 7 #include <cstdlib> | 7 #define INITGUID |
| 8 | |
| 9 #include <dbt.h> | |
| 10 #include <setupapi.h> | |
| 11 #include <winioctl.h> | |
| 8 | 12 |
| 9 #include "base/bind.h" | 13 #include "base/bind.h" |
| 10 #include "base/files/file.h" | 14 #include "base/files/file.h" |
| 11 #include "base/location.h" | 15 #include "base/location.h" |
| 12 #include "base/single_thread_task_runner.h" | 16 #include "base/single_thread_task_runner.h" |
| 13 #include "base/stl_util.h" | 17 #include "base/strings/string_util.h" |
| 14 #include "base/strings/sys_string_conversions.h" | 18 #include "base/strings/sys_string_conversions.h" |
| 15 #include "base/thread_task_runner_handle.h" | 19 #include "base/thread_task_runner_handle.h" |
| 16 #include "base/threading/thread_restrictions.h" | 20 #include "base/threading/thread_restrictions.h" |
| 21 #include "base/win/message_window.h" | |
| 17 #include "device/hid/hid_connection_win.h" | 22 #include "device/hid/hid_connection_win.h" |
| 18 #include "device/hid/hid_device_info.h" | 23 #include "device/hid/hid_device_info.h" |
| 19 #include "net/base/io_buffer.h" | 24 #include "net/base/io_buffer.h" |
| 20 | 25 |
| 21 #if defined(OS_WIN) | |
| 22 | |
| 23 #define INITGUID | |
| 24 | |
| 25 #include <setupapi.h> | |
| 26 #include <winioctl.h> | |
| 27 #include "base/win/scoped_handle.h" | |
| 28 | |
| 29 #endif // defined(OS_WIN) | |
| 30 | |
| 31 // Setup API is required to enumerate HID devices. | 26 // Setup API is required to enumerate HID devices. |
| 32 #pragma comment(lib, "setupapi.lib") | 27 #pragma comment(lib, "setupapi.lib") |
| 33 #pragma comment(lib, "hid.lib") | 28 #pragma comment(lib, "hid.lib") |
| 34 | 29 |
| 35 namespace device { | 30 namespace device { |
| 36 namespace { | 31 namespace { |
| 37 | 32 |
| 38 const char kHIDClass[] = "HIDClass"; | 33 const char kHIDClass[] = "HIDClass"; |
| 34 const wchar_t kWindowClassName[] = L"HidServiceMessageWindow"; | |
| 39 | 35 |
| 40 } // namespace | 36 } // namespace |
| 41 | 37 |
| 42 HidServiceWin::HidServiceWin() { | 38 HidServiceWin::HidServiceWin() { |
| 43 task_runner_ = base::ThreadTaskRunnerHandle::Get(); | 39 task_runner_ = base::ThreadTaskRunnerHandle::Get(); |
| 44 DCHECK(task_runner_.get()); | 40 DCHECK(task_runner_.get()); |
| 45 Enumerate(); | 41 RegisterForDeviceNotifications(); |
| 42 DoInitialEnumeration(); | |
| 46 } | 43 } |
| 47 | 44 |
| 48 HidServiceWin::~HidServiceWin() {} | 45 void HidServiceWin::Connect(const HidDeviceId& device_id, |
| 46 const ConnectCallback& callback) { | |
| 47 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 48 const auto& map_entry = devices().find(device_id); | |
| 49 if (map_entry == devices().end()) { | |
| 50 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); | |
| 51 return; | |
| 52 } | |
| 53 const HidDeviceInfo& device_info = map_entry->second; | |
| 49 | 54 |
| 50 void HidServiceWin::Enumerate() { | 55 base::win::ScopedHandle file(OpenDevice(device_info.device_id)); |
| 51 BOOL res; | 56 if (!file.IsValid()) { |
| 52 HDEVINFO device_info_set; | 57 PLOG(ERROR) << "Failed to open device"; |
| 53 SP_DEVINFO_DATA devinfo_data; | 58 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); |
| 54 SP_DEVICE_INTERFACE_DATA device_interface_data; | 59 return; |
| 60 } | |
| 55 | 61 |
| 56 memset(&devinfo_data, 0, sizeof(SP_DEVINFO_DATA)); | 62 task_runner_->PostTask( |
| 57 devinfo_data.cbSize = sizeof(SP_DEVINFO_DATA); | 63 FROM_HERE, |
| 58 device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); | 64 base::Bind(callback, new HidConnectionWin(device_info, file.Pass()))); |
| 65 } | |
| 59 | 66 |
| 60 device_info_set = SetupDiGetClassDevs( | 67 HidServiceWin::~HidServiceWin() { |
| 61 &GUID_DEVINTERFACE_HID, | 68 if (notify_handle_) { |
| 62 NULL, | 69 UnregisterDeviceNotification(notify_handle_); |
| 63 NULL, | 70 } |
| 64 DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); | 71 } |
| 65 | 72 |
| 66 std::set<std::string> connected_devices; | 73 void HidServiceWin::RegisterForDeviceNotifications() { |
| 74 window_.reset(new base::win::MessageWindow()); | |
|
Ken Rockot(use gerrit already)
2014/12/05 23:27:53
nit: Maybe file a bug + add a TODO which expresses
| |
| 75 if (!window_->CreateNamed( | |
| 76 base::Bind(&HidServiceWin::HandleMessage, base::Unretained(this)), | |
| 77 base::string16(kWindowClassName))) { | |
| 78 LOG(ERROR) << "Failed to create message window: " << kWindowClassName; | |
| 79 window_.reset(); | |
| 80 } | |
| 81 DEV_BROADCAST_DEVICEINTERFACE db = { sizeof(DEV_BROADCAST_DEVICEINTERFACE), | |
| 82 DBT_DEVTYP_DEVICEINTERFACE, | |
| 83 0, | |
| 84 GUID_DEVINTERFACE_HID }; | |
| 85 notify_handle_ = RegisterDeviceNotification(window_->hwnd(), &db, | |
| 86 DEVICE_NOTIFY_WINDOW_HANDLE); | |
| 87 if (!notify_handle_) { | |
| 88 LOG(ERROR) << "Failed to register for device notifications."; | |
| 89 window_.reset(); | |
| 90 } | |
| 91 } | |
| 92 | |
| 93 bool HidServiceWin::HandleMessage(UINT message, | |
| 94 WPARAM wparam, | |
| 95 LPARAM lparam, | |
| 96 LRESULT* result) { | |
| 97 if (message == WM_DEVICECHANGE) { | |
| 98 DEV_BROADCAST_DEVICEINTERFACE* db = | |
| 99 reinterpret_cast<DEV_BROADCAST_DEVICEINTERFACE*>(lparam); | |
| 100 std::string device_path(base::SysWideToUTF8(db->dbcc_name)); | |
| 101 DCHECK(base::IsStringASCII(device_path)); | |
| 102 if (wparam == DBT_DEVICEARRIVAL) { | |
| 103 PlatformAddDevice(base::StringToLowerASCII(device_path)); | |
| 104 } else if (wparam == DBT_DEVICEREMOVECOMPLETE) { | |
| 105 PlatformRemoveDevice(base::StringToLowerASCII(device_path)); | |
| 106 } | |
| 107 *result = NULL; | |
| 108 return true; | |
| 109 } | |
| 110 return false; | |
| 111 } | |
| 112 | |
| 113 void HidServiceWin::DoInitialEnumeration() { | |
| 114 HDEVINFO device_info_set = | |
| 115 SetupDiGetClassDevs(&GUID_DEVINTERFACE_HID, NULL, NULL, | |
| 116 DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); | |
| 67 | 117 |
| 68 if (device_info_set != INVALID_HANDLE_VALUE) { | 118 if (device_info_set != INVALID_HANDLE_VALUE) { |
| 119 SP_DEVICE_INTERFACE_DATA device_interface_data; | |
| 120 device_interface_data.cbSize = sizeof(SP_DEVICE_INTERFACE_DATA); | |
| 121 | |
| 69 for (int device_index = 0; | 122 for (int device_index = 0; |
| 70 SetupDiEnumDeviceInterfaces(device_info_set, | 123 SetupDiEnumDeviceInterfaces(device_info_set, |
| 71 NULL, | 124 NULL, |
| 72 &GUID_DEVINTERFACE_HID, | 125 &GUID_DEVINTERFACE_HID, |
| 73 device_index, | 126 device_index, |
| 74 &device_interface_data); | 127 &device_interface_data); |
| 75 ++device_index) { | 128 ++device_index) { |
| 76 DWORD required_size = 0; | 129 DWORD required_size = 0; |
| 77 | 130 |
| 78 // Determime the required size of detail struct. | 131 // Determime the required size of detail struct. |
| 79 SetupDiGetDeviceInterfaceDetailA(device_info_set, | 132 SetupDiGetDeviceInterfaceDetail(device_info_set, &device_interface_data, |
| 80 &device_interface_data, | 133 NULL, 0, &required_size, NULL); |
| 81 NULL, | |
| 82 0, | |
| 83 &required_size, | |
| 84 NULL); | |
| 85 | 134 |
| 86 scoped_ptr<SP_DEVICE_INTERFACE_DETAIL_DATA_A, base::FreeDeleter> | 135 scoped_ptr<SP_DEVICE_INTERFACE_DETAIL_DATA, base::FreeDeleter> |
| 87 device_interface_detail_data( | 136 device_interface_detail_data( |
| 88 static_cast<SP_DEVICE_INTERFACE_DETAIL_DATA_A*>( | 137 static_cast<SP_DEVICE_INTERFACE_DETAIL_DATA*>(malloc(required_size))); |
| 89 malloc(required_size))); | |
| 90 device_interface_detail_data->cbSize = | 138 device_interface_detail_data->cbSize = |
| 91 sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); | 139 sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); |
| 92 | 140 |
| 93 // Get the detailed data for this device. | 141 // Get the detailed data for this device. |
| 94 res = SetupDiGetDeviceInterfaceDetailA(device_info_set, | 142 BOOL res = SetupDiGetDeviceInterfaceDetail( |
| 95 &device_interface_data, | 143 device_info_set, &device_interface_data, |
| 96 device_interface_detail_data.get(), | 144 device_interface_detail_data.get(), required_size, NULL, NULL); |
| 97 required_size, | 145 if (!res) { |
| 98 NULL, | |
| 99 NULL); | |
| 100 if (!res) | |
| 101 continue; | 146 continue; |
| 102 | |
| 103 // Enumerate device info. Looking for Setup Class "HIDClass". | |
| 104 for (DWORD i = 0; | |
| 105 SetupDiEnumDeviceInfo(device_info_set, i, &devinfo_data); | |
| 106 i++) { | |
| 107 char class_name[256] = {0}; | |
| 108 res = SetupDiGetDeviceRegistryPropertyA(device_info_set, | |
| 109 &devinfo_data, | |
| 110 SPDRP_CLASS, | |
| 111 NULL, | |
| 112 (PBYTE) class_name, | |
| 113 sizeof(class_name) - 1, | |
| 114 NULL); | |
| 115 if (!res) | |
| 116 break; | |
| 117 if (memcmp(class_name, kHIDClass, sizeof(kHIDClass)) == 0) { | |
| 118 char driver_name[256] = {0}; | |
| 119 // Get bounded driver. | |
| 120 res = SetupDiGetDeviceRegistryPropertyA(device_info_set, | |
| 121 &devinfo_data, | |
| 122 SPDRP_DRIVER, | |
| 123 NULL, | |
| 124 (PBYTE) driver_name, | |
| 125 sizeof(driver_name) - 1, | |
| 126 NULL); | |
| 127 if (res) { | |
| 128 // Found the driver. | |
| 129 break; | |
| 130 } | |
| 131 } | |
| 132 } | 147 } |
| 133 | 148 |
| 134 if (!res) | 149 std::string device_path( |
| 135 continue; | 150 base::SysWideToUTF8(device_interface_detail_data->DevicePath)); |
| 136 | 151 DCHECK(base::IsStringASCII(device_path)); |
| 137 PlatformAddDevice(device_interface_detail_data->DevicePath); | 152 PlatformAddDevice(device_path); |
| 138 connected_devices.insert(device_interface_detail_data->DevicePath); | |
| 139 } | 153 } |
| 140 } | 154 } |
| 141 | |
| 142 // Find disconnected devices. | |
| 143 std::vector<std::string> disconnected_devices; | |
| 144 for (DeviceMap::const_iterator it = devices().begin(); it != devices().end(); | |
| 145 ++it) { | |
| 146 if (!ContainsKey(connected_devices, it->first)) { | |
| 147 disconnected_devices.push_back(it->first); | |
| 148 } | |
| 149 } | |
| 150 | |
| 151 // Remove disconnected devices. | |
| 152 for (size_t i = 0; i < disconnected_devices.size(); ++i) { | |
| 153 PlatformRemoveDevice(disconnected_devices[i]); | |
| 154 } | |
| 155 } | 155 } |
| 156 | 156 |
| 157 // static | |
| 157 void HidServiceWin::CollectInfoFromButtonCaps( | 158 void HidServiceWin::CollectInfoFromButtonCaps( |
| 158 PHIDP_PREPARSED_DATA preparsed_data, | 159 PHIDP_PREPARSED_DATA preparsed_data, |
| 159 HIDP_REPORT_TYPE report_type, | 160 HIDP_REPORT_TYPE report_type, |
| 160 USHORT button_caps_length, | 161 USHORT button_caps_length, |
| 161 HidCollectionInfo* collection_info) { | 162 HidCollectionInfo* collection_info) { |
| 162 if (button_caps_length > 0) { | 163 if (button_caps_length > 0) { |
| 163 scoped_ptr<HIDP_BUTTON_CAPS[]> button_caps( | 164 scoped_ptr<HIDP_BUTTON_CAPS[]> button_caps( |
| 164 new HIDP_BUTTON_CAPS[button_caps_length]); | 165 new HIDP_BUTTON_CAPS[button_caps_length]); |
| 165 if (HidP_GetButtonCaps(report_type, | 166 if (HidP_GetButtonCaps(report_type, |
| 166 &button_caps[0], | 167 &button_caps[0], |
| 167 &button_caps_length, | 168 &button_caps_length, |
| 168 preparsed_data) == HIDP_STATUS_SUCCESS) { | 169 preparsed_data) == HIDP_STATUS_SUCCESS) { |
| 169 for (size_t i = 0; i < button_caps_length; i++) { | 170 for (size_t i = 0; i < button_caps_length; i++) { |
| 170 int report_id = button_caps[i].ReportID; | 171 int report_id = button_caps[i].ReportID; |
| 171 if (report_id != 0) { | 172 if (report_id != 0) { |
| 172 collection_info->report_ids.insert(report_id); | 173 collection_info->report_ids.insert(report_id); |
| 173 } | 174 } |
| 174 } | 175 } |
| 175 } | 176 } |
| 176 } | 177 } |
| 177 } | 178 } |
| 178 | 179 |
| 180 // static | |
| 179 void HidServiceWin::CollectInfoFromValueCaps( | 181 void HidServiceWin::CollectInfoFromValueCaps( |
| 180 PHIDP_PREPARSED_DATA preparsed_data, | 182 PHIDP_PREPARSED_DATA preparsed_data, |
| 181 HIDP_REPORT_TYPE report_type, | 183 HIDP_REPORT_TYPE report_type, |
| 182 USHORT value_caps_length, | 184 USHORT value_caps_length, |
| 183 HidCollectionInfo* collection_info) { | 185 HidCollectionInfo* collection_info) { |
| 184 if (value_caps_length > 0) { | 186 if (value_caps_length > 0) { |
| 185 scoped_ptr<HIDP_VALUE_CAPS[]> value_caps( | 187 scoped_ptr<HIDP_VALUE_CAPS[]> value_caps( |
| 186 new HIDP_VALUE_CAPS[value_caps_length]); | 188 new HIDP_VALUE_CAPS[value_caps_length]); |
| 187 if (HidP_GetValueCaps( | 189 if (HidP_GetValueCaps( |
| 188 report_type, &value_caps[0], &value_caps_length, preparsed_data) == | 190 report_type, &value_caps[0], &value_caps_length, preparsed_data) == |
| 189 HIDP_STATUS_SUCCESS) { | 191 HIDP_STATUS_SUCCESS) { |
| 190 for (size_t i = 0; i < value_caps_length; i++) { | 192 for (size_t i = 0; i < value_caps_length; i++) { |
| 191 int report_id = value_caps[i].ReportID; | 193 int report_id = value_caps[i].ReportID; |
| 192 if (report_id != 0) { | 194 if (report_id != 0) { |
| 193 collection_info->report_ids.insert(report_id); | 195 collection_info->report_ids.insert(report_id); |
| 194 } | 196 } |
| 195 } | 197 } |
| 196 } | 198 } |
| 197 } | 199 } |
| 198 } | 200 } |
| 199 | 201 |
| 200 void HidServiceWin::PlatformAddDevice(const std::string& device_path) { | 202 void HidServiceWin::PlatformAddDevice(const std::string& device_path) { |
| 201 HidDeviceInfo device_info; | 203 HidDeviceInfo device_info; |
| 202 device_info.device_id = device_path; | 204 device_info.device_id = device_path; |
| 203 | 205 |
| 204 // Try to open the device. | 206 // Try to open the device. |
| 205 base::win::ScopedHandle device_handle( | 207 base::win::ScopedHandle device_handle(OpenDevice(device_path)); |
| 206 CreateFileA(device_path.c_str(), | 208 if (!device_handle.IsValid()) { |
| 207 GENERIC_WRITE | GENERIC_READ, | 209 return; |
| 208 FILE_SHARE_READ | FILE_SHARE_WRITE, | |
| 209 NULL, | |
| 210 OPEN_EXISTING, | |
| 211 FILE_FLAG_OVERLAPPED, | |
| 212 0)); | |
| 213 | |
| 214 if (!device_handle.IsValid() && | |
| 215 GetLastError() == base::File::FILE_ERROR_ACCESS_DENIED) { | |
| 216 base::win::ScopedHandle device_handle( | |
| 217 CreateFileA(device_path.c_str(), | |
| 218 GENERIC_READ, | |
| 219 FILE_SHARE_READ, | |
| 220 NULL, | |
| 221 OPEN_EXISTING, | |
| 222 FILE_FLAG_OVERLAPPED, | |
| 223 0)); | |
| 224 | |
| 225 if (!device_handle.IsValid()) | |
| 226 return; | |
| 227 } | 210 } |
| 228 | 211 |
| 229 // Get VID/PID pair. | 212 // Get VID/PID pair. |
| 230 HIDD_ATTRIBUTES attrib = {0}; | 213 HIDD_ATTRIBUTES attrib = {0}; |
| 231 attrib.Size = sizeof(HIDD_ATTRIBUTES); | 214 attrib.Size = sizeof(HIDD_ATTRIBUTES); |
| 232 if (!HidD_GetAttributes(device_handle.Get(), &attrib)) | 215 if (!HidD_GetAttributes(device_handle.Get(), &attrib)) { |
| 233 return; | 216 return; |
| 217 } | |
| 234 | 218 |
| 235 device_info.vendor_id = attrib.VendorID; | 219 device_info.vendor_id = attrib.VendorID; |
| 236 device_info.product_id = attrib.ProductID; | 220 device_info.product_id = attrib.ProductID; |
| 237 | 221 |
| 238 for (ULONG i = 32; | |
| 239 HidD_SetNumInputBuffers(device_handle.Get(), i); | |
| 240 i <<= 1); | |
| 241 | |
| 242 // Get usage and usage page (optional). | 222 // Get usage and usage page (optional). |
| 243 PHIDP_PREPARSED_DATA preparsed_data; | 223 PHIDP_PREPARSED_DATA preparsed_data; |
| 244 if (HidD_GetPreparsedData(device_handle.Get(), &preparsed_data) && | 224 if (HidD_GetPreparsedData(device_handle.Get(), &preparsed_data) && |
| 245 preparsed_data) { | 225 preparsed_data) { |
| 246 HIDP_CAPS capabilities = {0}; | 226 HIDP_CAPS capabilities = {0}; |
| 247 if (HidP_GetCaps(preparsed_data, &capabilities) == HIDP_STATUS_SUCCESS) { | 227 if (HidP_GetCaps(preparsed_data, &capabilities) == HIDP_STATUS_SUCCESS) { |
| 248 device_info.max_input_report_size = capabilities.InputReportByteLength; | 228 device_info.max_input_report_size = capabilities.InputReportByteLength; |
| 249 device_info.max_output_report_size = capabilities.OutputReportByteLength; | 229 device_info.max_output_report_size = capabilities.OutputReportByteLength; |
| 250 device_info.max_feature_report_size = | 230 device_info.max_feature_report_size = |
| 251 capabilities.FeatureReportByteLength; | 231 capabilities.FeatureReportByteLength; |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 297 HidD_FreePreparsedData(preparsed_data); | 277 HidD_FreePreparsedData(preparsed_data); |
| 298 } | 278 } |
| 299 | 279 |
| 300 AddDevice(device_info); | 280 AddDevice(device_info); |
| 301 } | 281 } |
| 302 | 282 |
| 303 void HidServiceWin::PlatformRemoveDevice(const std::string& device_path) { | 283 void HidServiceWin::PlatformRemoveDevice(const std::string& device_path) { |
| 304 RemoveDevice(device_path); | 284 RemoveDevice(device_path); |
| 305 } | 285 } |
| 306 | 286 |
| 307 void HidServiceWin::GetDevices(std::vector<HidDeviceInfo>* devices) { | 287 base::win::ScopedHandle HidServiceWin::OpenDevice( |
| 308 Enumerate(); | 288 const std::string& device_path) { |
| 309 HidService::GetDevices(devices); | 289 base::win::ScopedHandle file( |
| 310 } | 290 CreateFileA(device_path.c_str(), GENERIC_WRITE | GENERIC_READ, |
| 311 | 291 FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, |
| 312 void HidServiceWin::Connect(const HidDeviceId& device_id, | 292 FILE_FLAG_OVERLAPPED, NULL)); |
| 313 const ConnectCallback& callback) { | 293 if (!file.IsValid() && |
| 314 DCHECK(thread_checker_.CalledOnValidThread()); | 294 GetLastError() == base::File::FILE_ERROR_ACCESS_DENIED) { |
|
Nico
2015/12/09 17:05:45
Are you sure this is correct? The CreateFile() MSD
Reilly Grant (use Gerrit)
2015/12/09 18:49:29
Interesting. I wonder if this logic has ever been
Nico
2015/12/09 19:10:53
Deleting this block if it's broken and untested so
| |
| 315 const auto& map_entry = devices().find(device_id); | 295 file.Set(CreateFileA(device_path.c_str(), GENERIC_READ, FILE_SHARE_READ, |
| 316 if (map_entry == devices().end()) { | 296 NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL)); |
| 317 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); | |
| 318 return; | |
| 319 } | 297 } |
| 320 const HidDeviceInfo& device_info = map_entry->second; | 298 return file.Pass(); |
| 321 | |
| 322 scoped_refptr<HidConnectionWin> connection(new HidConnectionWin(device_info)); | |
| 323 if (!connection->available()) { | |
| 324 PLOG(ERROR) << "Failed to open device"; | |
| 325 connection = nullptr; | |
| 326 } | |
| 327 task_runner_->PostTask(FROM_HERE, base::Bind(callback, connection)); | |
| 328 } | 299 } |
| 329 | 300 |
| 330 } // namespace device | 301 } // namespace device |
| OLD | NEW |