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_linux.h" | 5 #include "device/hid/hid_service_linux.h" |
| 6 | 6 |
| 7 #include <fcntl.h> | |
| 7 #include <string> | 8 #include <string> |
| 8 | 9 |
| 9 #include "base/bind.h" | 10 #include "base/bind.h" |
| 10 #include "base/files/file.h" | 11 #include "base/files/file.h" |
| 11 #include "base/files/file_path.h" | 12 #include "base/files/file_path.h" |
| 12 #include "base/files/file_util.h" | 13 #include "base/files/file_util.h" |
| 14 #include "base/location.h" | |
| 13 #include "base/logging.h" | 15 #include "base/logging.h" |
| 14 #include "base/stl_util.h" | 16 #include "base/scoped_observer.h" |
| 15 #include "base/strings/string_number_conversions.h" | 17 #include "base/strings/string_number_conversions.h" |
| 16 #include "base/strings/string_piece.h" | |
| 17 #include "base/strings/string_split.h" | 18 #include "base/strings/string_split.h" |
| 18 #include "base/thread_task_runner_handle.h" | 19 #include "base/thread_task_runner_handle.h" |
| 19 #include "base/threading/thread_restrictions.h" | 20 #include "base/threading/thread_restrictions.h" |
| 21 #include "device/hid/device_monitor_linux.h" | |
| 20 #include "device/hid/hid_connection_linux.h" | 22 #include "device/hid/hid_connection_linux.h" |
| 21 #include "device/hid/hid_device_info.h" | 23 #include "device/hid/hid_device_info.h" |
| 22 #include "device/hid/hid_report_descriptor.h" | 24 #include "device/hid/hid_report_descriptor.h" |
| 23 #include "device/udev_linux/scoped_udev.h" | 25 #include "device/udev_linux/scoped_udev.h" |
| 24 | 26 |
| 25 #if defined(OS_CHROMEOS) | 27 #if defined(OS_CHROMEOS) |
| 26 #include "base/sys_info.h" | 28 #include "base/sys_info.h" |
| 27 #include "chromeos/dbus/dbus_thread_manager.h" | 29 #include "chromeos/dbus/dbus_thread_manager.h" |
| 28 #include "chromeos/dbus/permission_broker_client.h" | 30 #include "chromeos/dbus/permission_broker_client.h" |
| 29 #endif // defined(OS_CHROMEOS) | 31 #endif // defined(OS_CHROMEOS) |
| 30 | 32 |
| 31 namespace device { | 33 namespace device { |
| 32 | 34 |
| 33 namespace { | 35 namespace { |
| 34 | 36 |
| 35 const char kHidrawSubsystem[] = "hidraw"; | 37 const char kHidrawSubsystem[] = "hidraw"; |
| 36 const char kHIDID[] = "HID_ID"; | 38 const char kHIDID[] = "HID_ID"; |
| 37 const char kHIDName[] = "HID_NAME"; | 39 const char kHIDName[] = "HID_NAME"; |
| 38 const char kHIDUnique[] = "HID_UNIQ"; | 40 const char kHIDUnique[] = "HID_UNIQ"; |
| 39 const char kSysfsReportDescriptorKey[] = "report_descriptor"; | 41 const char kSysfsReportDescriptorKey[] = "report_descriptor"; |
| 40 | 42 |
| 43 } // namespace | |
| 44 | |
| 45 struct HidServiceLinux::ConnectParams { | |
| 46 ConnectParams(const HidDeviceInfo& device_info, | |
| 47 const ConnectCallback& callback, | |
| 48 scoped_refptr<base::SingleThreadTaskRunner> task_runner, | |
| 49 scoped_refptr<base::SingleThreadTaskRunner> file_task_runner) | |
| 50 : device_info(device_info), | |
| 51 callback(callback), | |
| 52 task_runner(task_runner), | |
| 53 file_task_runner(file_task_runner) {} | |
| 54 ~ConnectParams() {} | |
| 55 | |
| 56 HidDeviceInfo device_info; | |
| 57 ConnectCallback callback; | |
| 58 scoped_refptr<base::SingleThreadTaskRunner> task_runner; | |
| 59 scoped_refptr<base::SingleThreadTaskRunner> file_task_runner; | |
| 60 base::File device_file; | |
| 61 }; | |
| 62 | |
| 63 class HidServiceLinux::Helper : public DeviceMonitorLinux::Observer, | |
| 64 public base::MessageLoop::DestructionObserver, | |
| 65 public base::NonThreadSafe { | |
| 66 public: | |
| 67 Helper(base::WeakPtr<HidServiceLinux> service, | |
| 68 scoped_refptr<base::SingleThreadTaskRunner> task_runner) | |
| 69 : observer_(this), service_(service), task_runner_(task_runner) { | |
| 70 DeviceMonitorLinux* monitor = DeviceMonitorLinux::GetInstance(); | |
| 71 observer_.Add(monitor); | |
| 72 monitor->Enumerate( | |
| 73 base::Bind(&Helper::OnDeviceAdded, base::Unretained(this))); | |
| 74 } | |
| 75 | |
| 76 virtual ~Helper() {} | |
| 77 | |
| 78 private: | |
| 79 // DeviceMonitorLinux::Observer: | |
| 80 void OnDeviceAdded(udev_device* device) override { | |
| 81 const char* device_path = udev_device_get_syspath(device); | |
| 82 if (!device_path) { | |
| 83 return; | |
| 84 } | |
| 85 const char* subsystem = udev_device_get_subsystem(device); | |
| 86 if (!subsystem || strcmp(subsystem, kHidrawSubsystem) != 0) { | |
| 87 return; | |
| 88 } | |
| 89 | |
| 90 HidDeviceInfo device_info; | |
| 91 device_info.device_id = device_path; | |
| 92 | |
| 93 const char* str_property = udev_device_get_devnode(device); | |
| 94 if (!str_property) { | |
| 95 return; | |
| 96 } | |
| 97 device_info.device_node = str_property; | |
| 98 | |
| 99 udev_device* parent = udev_device_get_parent(device); | |
| 100 if (!parent) { | |
| 101 return; | |
| 102 } | |
| 103 | |
| 104 const char* hid_id = udev_device_get_property_value(parent, kHIDID); | |
| 105 if (!hid_id) { | |
| 106 return; | |
| 107 } | |
| 108 | |
| 109 std::vector<std::string> parts; | |
| 110 base::SplitString(hid_id, ':', &parts); | |
| 111 if (parts.size() != 3) { | |
| 112 return; | |
| 113 } | |
| 114 | |
| 115 uint32_t int_property = 0; | |
| 116 if (HexStringToUInt(base::StringPiece(parts[1]), &int_property)) { | |
| 117 device_info.vendor_id = int_property; | |
| 118 } | |
| 119 | |
| 120 if (HexStringToUInt(base::StringPiece(parts[2]), &int_property)) { | |
| 121 device_info.product_id = int_property; | |
| 122 } | |
| 123 | |
| 124 str_property = udev_device_get_property_value(parent, kHIDUnique); | |
| 125 if (str_property != NULL) { | |
|
Ken Rockot(use gerrit already)
2014/12/03 22:35:10
nit: could you nullptr these while you're here?
Reilly Grant (use Gerrit)
2014/12/03 23:10:28
Technically this is a C API and so NULL is correct
| |
| 126 device_info.serial_number = str_property; | |
| 127 } | |
| 128 | |
| 129 str_property = udev_device_get_property_value(parent, kHIDName); | |
| 130 if (str_property != NULL) { | |
| 131 device_info.product_name = str_property; | |
| 132 } | |
| 133 | |
| 134 const char* parent_sysfs_path = udev_device_get_syspath(parent); | |
| 135 if (!parent_sysfs_path) { | |
| 136 return; | |
| 137 } | |
| 138 base::FilePath report_descriptor_path = | |
| 139 base::FilePath(parent_sysfs_path).Append(kSysfsReportDescriptorKey); | |
| 140 std::string report_descriptor_str; | |
| 141 if (!base::ReadFileToString(report_descriptor_path, | |
| 142 &report_descriptor_str)) { | |
| 143 return; | |
| 144 } | |
| 145 | |
| 146 HidReportDescriptor report_descriptor( | |
| 147 reinterpret_cast<uint8_t*>(&report_descriptor_str[0]), | |
| 148 report_descriptor_str.length()); | |
| 149 report_descriptor.GetDetails( | |
| 150 &device_info.collections, &device_info.has_report_id, | |
| 151 &device_info.max_input_report_size, &device_info.max_output_report_size, | |
| 152 &device_info.max_feature_report_size); | |
| 153 | |
| 154 task_runner_->PostTask(FROM_HERE, base::Bind(&HidServiceLinux::AddDevice, | |
| 155 service_, device_info)); | |
| 156 } | |
| 157 | |
| 158 void OnDeviceRemoved(udev_device* device) override { | |
| 159 const char* device_path = udev_device_get_syspath(device); | |
| 160 if (device_path) { | |
| 161 task_runner_->PostTask( | |
| 162 FROM_HERE, | |
| 163 base::Bind(&HidServiceLinux::RemoveDevice, service_, device_path)); | |
| 164 } | |
| 165 } | |
| 166 | |
| 167 // base::MessageLoop::DestructionObserver: | |
| 168 void WillDestroyCurrentMessageLoop() override { | |
| 169 base::MessageLoop::current()->RemoveDestructionObserver(this); | |
| 170 delete this; | |
| 171 } | |
| 172 | |
| 173 ScopedObserver<DeviceMonitorLinux, DeviceMonitorLinux::Observer> observer_; | |
| 174 | |
| 175 // This weak pointer is only valid when checked on this task runner. | |
| 176 base::WeakPtr<HidServiceLinux> service_; | |
| 177 scoped_refptr<base::SingleThreadTaskRunner> task_runner_; | |
| 178 }; | |
| 179 | |
| 180 HidServiceLinux::HidServiceLinux( | |
| 181 scoped_refptr<base::SingleThreadTaskRunner> file_task_runner) | |
| 182 : file_task_runner_(file_task_runner), weak_factory_(this) { | |
| 183 task_runner_ = base::ThreadTaskRunnerHandle::Get(); | |
| 184 // The device watcher is passed a weak pointer back to this service so that it | |
| 185 // can be cleaned up after the service is destroyed however this weak pointer | |
| 186 // must be constructed on the this thread where it will be checked. | |
| 187 file_task_runner_->PostTask( | |
| 188 FROM_HERE, base::Bind(&HidServiceLinux::StartHelper, | |
| 189 weak_factory_.GetWeakPtr(), task_runner_)); | |
| 190 } | |
| 191 | |
| 192 // static | |
| 193 void HidServiceLinux::StartHelper( | |
| 194 base::WeakPtr<HidServiceLinux> weak_ptr, | |
| 195 scoped_refptr<base::SingleThreadTaskRunner> task_runner) { | |
| 196 // Helper is a message loop destruction observer and will delete itself when | |
| 197 // this thread's message loop is destroyed. | |
| 198 new Helper(weak_ptr, task_runner); | |
| 199 } | |
| 200 | |
| 201 void HidServiceLinux::Connect(const HidDeviceId& device_id, | |
| 202 const ConnectCallback& callback) { | |
| 203 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 204 | |
| 205 const auto& map_entry = devices().find(device_id); | |
| 206 if (map_entry == devices().end()) { | |
| 207 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); | |
| 208 return; | |
| 209 } | |
| 210 const HidDeviceInfo& device_info = map_entry->second; | |
| 211 | |
| 212 scoped_ptr<ConnectParams> params(new ConnectParams( | |
| 213 device_info, callback, task_runner_, file_task_runner_)); | |
| 214 | |
| 41 #if defined(OS_CHROMEOS) | 215 #if defined(OS_CHROMEOS) |
| 42 void OnRequestAccessComplete( | |
| 43 scoped_refptr<base::SingleThreadTaskRunner> reply_task_runner, | |
| 44 const base::Callback<void(bool success)>& callback, | |
| 45 bool success) { | |
| 46 reply_task_runner->PostTask(FROM_HERE, base::Bind(callback, success)); | |
| 47 } | |
| 48 | |
| 49 void RequestAccess( | |
| 50 const std::string& device_node, | |
| 51 scoped_refptr<base::SingleThreadTaskRunner> reply_task_runner, | |
| 52 const base::Callback<void(bool success)>& callback) { | |
| 53 bool success = false; | |
| 54 | |
| 55 if (base::SysInfo::IsRunningOnChromeOS()) { | 216 if (base::SysInfo::IsRunningOnChromeOS()) { |
| 56 chromeos::PermissionBrokerClient* client = | 217 chromeos::PermissionBrokerClient* client = |
| 57 chromeos::DBusThreadManager::Get()->GetPermissionBrokerClient(); | 218 chromeos::DBusThreadManager::Get()->GetPermissionBrokerClient(); |
| 58 DCHECK(client) << "Could not get permission broker client."; | 219 DCHECK(client) << "Could not get permission broker client."; |
| 59 if (client) { | 220 if (client) { |
| 60 client->RequestPathAccess( | 221 client->RequestPathAccess( |
| 61 device_node, | 222 device_info.device_node, -1, |
| 62 -1, | 223 base::Bind(&HidServiceLinux::OnRequestPathAccessComplete, |
| 63 base::Bind(OnRequestAccessComplete, reply_task_runner, callback)); | 224 base::Passed(¶ms))); |
| 64 return; | 225 } else { |
| 65 } | 226 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); |
| 227 } | |
| 228 return; | |
| 229 } | |
| 230 #endif // defined(OS_CHROMEOS) | |
| 231 | |
| 232 file_task_runner_->PostTask( | |
| 233 FROM_HERE, | |
| 234 base::Bind(&HidServiceLinux::OpenDevice, base::Passed(¶ms))); | |
| 235 } | |
| 236 | |
| 237 HidServiceLinux::~HidServiceLinux() { | |
| 238 file_task_runner_->DeleteSoon(FROM_HERE, helper_.release()); | |
| 239 } | |
| 240 | |
| 241 #if defined(OS_CHROMEOS) | |
| 242 // static | |
| 243 void HidServiceLinux::OnRequestPathAccessComplete( | |
| 244 scoped_ptr<ConnectParams> params, | |
| 245 bool success) { | |
| 246 if (success) { | |
| 247 scoped_refptr<base::SingleThreadTaskRunner> file_task_runner = | |
| 248 params->file_task_runner; | |
| 249 file_task_runner->PostTask( | |
| 250 FROM_HERE, | |
| 251 base::Bind(&HidServiceLinux::OpenDevice, base::Passed(¶ms))); | |
| 66 } else { | 252 } else { |
| 67 // Not really running on Chrome OS, declare success. | 253 params->callback.Run(nullptr); |
| 68 success = true; | 254 } |
| 69 } | 255 } |
| 70 | 256 #endif // defined(OS_CHROMEOS) |
| 71 reply_task_runner->PostTask(FROM_HERE, base::Bind(callback, success)); | 257 |
| 72 } | 258 // static |
| 73 #endif | 259 void HidServiceLinux::OpenDevice(scoped_ptr<ConnectParams> params) { |
| 74 | |
| 75 } // namespace | |
| 76 | |
| 77 HidServiceLinux::HidServiceLinux( | |
| 78 scoped_refptr<base::SingleThreadTaskRunner> ui_task_runner) | |
| 79 : ui_task_runner_(ui_task_runner), | |
| 80 weak_factory_(this) { | |
| 81 base::ThreadRestrictions::AssertIOAllowed(); | 260 base::ThreadRestrictions::AssertIOAllowed(); |
| 82 task_runner_ = base::ThreadTaskRunnerHandle::Get(); | 261 scoped_refptr<base::SingleThreadTaskRunner> task_runner = params->task_runner; |
| 83 DeviceMonitorLinux* monitor = DeviceMonitorLinux::GetInstance(); | 262 base::FilePath device_path(params->device_info.device_node); |
| 84 monitor->AddObserver(this); | 263 base::File& device_file = params->device_file; |
| 85 monitor->Enumerate( | 264 int flags = |
| 86 base::Bind(&HidServiceLinux::OnDeviceAdded, weak_factory_.GetWeakPtr())); | 265 base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE; |
| 87 } | 266 device_file.Initialize(device_path, flags); |
| 88 | 267 if (!device_file.IsValid()) { |
| 89 void HidServiceLinux::Connect(const HidDeviceId& device_id, | 268 base::File::Error file_error = device_file.error_details(); |
| 90 const ConnectCallback& callback) { | 269 |
| 91 DCHECK(thread_checker_.CalledOnValidThread()); | 270 if (file_error == base::File::FILE_ERROR_ACCESS_DENIED) { |
| 92 | 271 VLOG(1) << "Access denied opening device read-write, trying read-only."; |
| 93 ScopedUdevDevicePtr device = | 272 flags = base::File::FLAG_OPEN | base::File::FLAG_READ; |
| 94 DeviceMonitorLinux::GetInstance()->GetDeviceFromPath( | 273 device_file.Initialize(device_path, flags); |
| 95 device_id); | 274 } |
| 96 if (!device) { | 275 } |
| 97 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); | 276 if (!device_file.IsValid()) { |
| 98 return; | 277 LOG(ERROR) << "Failed to open '" << params->device_info.device_node << "': " |
| 99 } | 278 << base::File::ErrorToString(device_file.error_details()); |
| 100 | 279 task_runner->PostTask(FROM_HERE, base::Bind(params->callback, nullptr)); |
| 101 const char* device_node = udev_device_get_devnode(device.get()); | 280 return; |
| 102 if (!device_node) { | 281 } |
| 103 task_runner_->PostTask(FROM_HERE, base::Bind(callback, nullptr)); | 282 |
| 104 return; | 283 int result = fcntl(device_file.GetPlatformFile(), F_GETFL); |
| 105 } | 284 if (result == -1) { |
| 106 | 285 PLOG(ERROR) << "Failed to get flags from the device file descriptor"; |
| 107 base::Callback<void(bool success)> finish_connect = | 286 task_runner->PostTask(FROM_HERE, base::Bind(params->callback, nullptr)); |
| 108 base::Bind(&HidServiceLinux::FinishConnect, | 287 return; |
| 109 weak_factory_.GetWeakPtr(), | 288 } |
| 110 device_id, | 289 |
| 111 std::string(device_node), | 290 result = fcntl(device_file.GetPlatformFile(), F_SETFL, result | O_NONBLOCK); |
| 112 callback); | 291 if (result == -1) { |
| 113 | 292 PLOG(ERROR) << "Failed to set the non-blocking flag on the device fd"; |
| 114 #if defined(OS_CHROMEOS) | 293 task_runner->PostTask(FROM_HERE, base::Bind(params->callback, nullptr)); |
| 115 ui_task_runner_->PostTask(FROM_HERE, | 294 return; |
| 116 base::Bind(RequestAccess, | 295 } |
| 117 std::string(device_node), | 296 |
| 118 task_runner_, | 297 task_runner->PostTask(FROM_HERE, base::Bind(&HidServiceLinux::ConnectImpl, |
| 119 finish_connect)); | 298 base::Passed(¶ms))); |
| 120 #else | 299 } |
| 121 // Use the task runner to preserve the asynchronous behavior of this call on | 300 |
| 122 // non-Chrome OS platforms. | 301 // static |
| 123 task_runner_->PostTask(FROM_HERE, base::Bind(finish_connect, true)); | 302 void HidServiceLinux::ConnectImpl(scoped_ptr<ConnectParams> params) { |
| 124 #endif | 303 DCHECK(params->device_file.IsValid()); |
| 125 } | 304 params->callback.Run(make_scoped_refptr( |
| 126 | 305 new HidConnectionLinux(params->device_info, params->device_file.Pass(), |
| 127 HidServiceLinux::~HidServiceLinux() { | 306 params->file_task_runner))); |
| 128 if (DeviceMonitorLinux::HasInstance()) | |
| 129 DeviceMonitorLinux::GetInstance()->RemoveObserver(this); | |
| 130 } | |
| 131 | |
| 132 void HidServiceLinux::OnDeviceAdded(udev_device* device) { | |
| 133 if (!device) | |
| 134 return; | |
| 135 | |
| 136 const char* device_path = udev_device_get_syspath(device); | |
| 137 if (!device_path) | |
| 138 return; | |
| 139 const char* subsystem = udev_device_get_subsystem(device); | |
| 140 if (!subsystem || strcmp(subsystem, kHidrawSubsystem) != 0) | |
| 141 return; | |
| 142 | |
| 143 HidDeviceInfo device_info; | |
| 144 device_info.device_id = device_path; | |
| 145 | |
| 146 uint32_t int_property = 0; | |
| 147 const char* str_property = NULL; | |
| 148 | |
| 149 udev_device* parent = udev_device_get_parent(device); | |
| 150 if (!parent) { | |
| 151 return; | |
| 152 } | |
| 153 | |
| 154 const char* hid_id = udev_device_get_property_value(parent, kHIDID); | |
| 155 if (!hid_id) { | |
| 156 return; | |
| 157 } | |
| 158 | |
| 159 std::vector<std::string> parts; | |
| 160 base::SplitString(hid_id, ':', &parts); | |
| 161 if (parts.size() != 3) { | |
| 162 return; | |
| 163 } | |
| 164 | |
| 165 if (HexStringToUInt(base::StringPiece(parts[1]), &int_property)) { | |
| 166 device_info.vendor_id = int_property; | |
| 167 } | |
| 168 | |
| 169 if (HexStringToUInt(base::StringPiece(parts[2]), &int_property)) { | |
| 170 device_info.product_id = int_property; | |
| 171 } | |
| 172 | |
| 173 str_property = udev_device_get_property_value(parent, kHIDUnique); | |
| 174 if (str_property != NULL) { | |
| 175 device_info.serial_number = str_property; | |
| 176 } | |
| 177 | |
| 178 str_property = udev_device_get_property_value(parent, kHIDName); | |
| 179 if (str_property != NULL) { | |
| 180 device_info.product_name = str_property; | |
| 181 } | |
| 182 | |
| 183 const char* parent_sysfs_path = udev_device_get_syspath(parent); | |
| 184 if (!parent_sysfs_path) { | |
| 185 return; | |
| 186 } | |
| 187 base::FilePath report_descriptor_path = | |
| 188 base::FilePath(parent_sysfs_path).Append(kSysfsReportDescriptorKey); | |
| 189 std::string report_descriptor_str; | |
| 190 if (!base::ReadFileToString(report_descriptor_path, &report_descriptor_str)) { | |
| 191 return; | |
| 192 } | |
| 193 | |
| 194 HidReportDescriptor report_descriptor( | |
| 195 reinterpret_cast<uint8_t*>(&report_descriptor_str[0]), | |
| 196 report_descriptor_str.length()); | |
| 197 report_descriptor.GetDetails(&device_info.collections, | |
| 198 &device_info.has_report_id, | |
| 199 &device_info.max_input_report_size, | |
| 200 &device_info.max_output_report_size, | |
| 201 &device_info.max_feature_report_size); | |
| 202 | |
| 203 AddDevice(device_info); | |
| 204 } | |
| 205 | |
| 206 void HidServiceLinux::OnDeviceRemoved(udev_device* device) { | |
| 207 const char* device_path = udev_device_get_syspath(device);; | |
| 208 if (device_path) { | |
| 209 RemoveDevice(device_path); | |
| 210 } | |
| 211 } | |
| 212 | |
| 213 void HidServiceLinux::FinishConnect( | |
| 214 const HidDeviceId& device_id, | |
| 215 const std::string device_node, | |
| 216 const base::Callback<void(scoped_refptr<HidConnection>)>& callback, | |
| 217 bool success) { | |
| 218 DCHECK(thread_checker_.CalledOnValidThread()); | |
| 219 if (!success) { | |
| 220 callback.Run(nullptr); | |
| 221 } | |
| 222 | |
| 223 const auto& map_entry = devices().find(device_id); | |
| 224 if (map_entry == devices().end()) { | |
| 225 callback.Run(nullptr); | |
| 226 } | |
| 227 | |
| 228 callback.Run(new HidConnectionLinux(map_entry->second, device_node)); | |
| 229 } | 307 } |
| 230 | 308 |
| 231 } // namespace device | 309 } // namespace device |
| OLD | NEW |