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 "chrome/browser/extensions/api/bluetooth_socket/bluetooth_socket_event_ dispatcher.h" | 5 #include "chrome/browser/extensions/api/bluetooth_socket/bluetooth_socket_event_ dispatcher.h" |
| 6 | 6 |
| 7 #include "chrome/browser/browser_process.h" | 7 #include "chrome/browser/browser_process.h" |
| 8 #include "chrome/browser/extensions/api/bluetooth/bluetooth_api_socket.h" | 8 #include "chrome/browser/extensions/api/bluetooth/bluetooth_api_socket.h" |
| 9 #include "chrome/common/extensions/api/bluetooth_socket.h" | 9 #include "chrome/common/extensions/api/bluetooth_socket.h" |
| 10 #include "device/bluetooth/bluetooth_device.h" | |
| 11 #include "device/bluetooth/bluetooth_socket.h" | |
| 10 #include "extensions/browser/event_router.h" | 12 #include "extensions/browser/event_router.h" |
| 11 #include "net/base/io_buffer.h" | 13 #include "net/base/io_buffer.h" |
| 12 #include "net/base/net_errors.h" | 14 #include "net/base/net_errors.h" |
| 13 | 15 |
| 14 namespace { | 16 namespace { |
| 15 | 17 |
| 16 namespace bluetooth_socket = extensions::api::bluetooth_socket; | 18 namespace bluetooth_socket = extensions::api::bluetooth_socket; |
| 17 using extensions::BluetoothApiSocket; | 19 using extensions::BluetoothApiSocket; |
| 18 | 20 |
| 19 int kDefaultBufferSize = 4096; | 21 int kDefaultBufferSize = 4096; |
| 20 | 22 |
| 21 bluetooth_socket::ReceiveError MapErrorReason( | 23 bluetooth_socket::ReceiveError MapReceiveErrorReason( |
| 22 BluetoothApiSocket::ErrorReason value) { | 24 BluetoothApiSocket::ErrorReason value) { |
| 23 switch (value) { | 25 switch (value) { |
| 24 case BluetoothApiSocket::kDisconnected: | 26 case BluetoothApiSocket::kDisconnected: |
| 25 return bluetooth_socket::RECEIVE_ERROR_DISCONNECTED; | 27 return bluetooth_socket::RECEIVE_ERROR_DISCONNECTED; |
| 26 case BluetoothApiSocket::kNotConnected: | 28 case BluetoothApiSocket::kNotConnected: |
| 27 // kNotConnected is impossible since a socket has to be connected to be | 29 // kNotConnected is impossible since a socket has to be connected to be |
| 28 // able to call Receive() on it. | 30 // able to call Receive() on it. |
| 29 // fallthrough | 31 // fallthrough |
| 30 case BluetoothApiSocket::kIOPending: | 32 case BluetoothApiSocket::kIOPending: |
| 31 // kIOPending is not relevant to apps, as BluetoothSocketEventDispatcher | 33 // kIOPending is not relevant to apps, as BluetoothSocketEventDispatcher |
| 32 // handles this specific error. | 34 // handles this specific error. |
| 33 // fallthrough | 35 // fallthrough |
| 34 default: | 36 default: |
| 35 return bluetooth_socket::RECEIVE_ERROR_SYSTEM_ERROR; | 37 return bluetooth_socket::RECEIVE_ERROR_SYSTEM_ERROR; |
| 36 } | 38 } |
| 37 } | 39 } |
| 38 | 40 |
| 41 bluetooth_socket::AcceptError MapAcceptErrorReason( | |
| 42 BluetoothApiSocket::ErrorReason value) { | |
| 43 // TODO(keybuk): All values are system error, we may want to seperate these | |
| 44 // out to more discrete reasons. | |
| 45 switch (value) { | |
| 46 case BluetoothApiSocket::kNotListening: | |
| 47 // kNotListening is impossible since a socket has to be listening to be | |
| 48 // able to call Accept() on it. | |
| 49 // fallthrough | |
| 50 default: | |
| 51 return bluetooth_socket::ACCEPT_ERROR_SYSTEM_ERROR; | |
| 52 } | |
| 53 } | |
| 54 | |
| 39 } // namespace | 55 } // namespace |
| 40 | 56 |
| 41 namespace extensions { | 57 namespace extensions { |
| 42 namespace api { | 58 namespace api { |
| 43 | 59 |
| 44 using content::BrowserThread; | 60 using content::BrowserThread; |
| 45 | 61 |
| 46 static base::LazyInstance< | 62 static base::LazyInstance< |
| 47 BrowserContextKeyedAPIFactory<BluetoothSocketEventDispatcher> > g_factory = | 63 BrowserContextKeyedAPIFactory<BluetoothSocketEventDispatcher> > g_factory = |
| 48 LAZY_INSTANCE_INITIALIZER; | 64 LAZY_INSTANCE_INITIALIZER; |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 71 DCHECK(manager) | 87 DCHECK(manager) |
| 72 << "There is no socket manager. " | 88 << "There is no socket manager. " |
| 73 "If this assertion is failing during a test, then it is likely that " | 89 "If this assertion is failing during a test, then it is likely that " |
| 74 "TestExtensionSystem is failing to provide an instance of " | 90 "TestExtensionSystem is failing to provide an instance of " |
| 75 "ApiResourceManager<BluetoothApiSocket>."; | 91 "ApiResourceManager<BluetoothApiSocket>."; |
| 76 sockets_ = manager->data_; | 92 sockets_ = manager->data_; |
| 77 } | 93 } |
| 78 | 94 |
| 79 BluetoothSocketEventDispatcher::~BluetoothSocketEventDispatcher() {} | 95 BluetoothSocketEventDispatcher::~BluetoothSocketEventDispatcher() {} |
| 80 | 96 |
| 81 BluetoothSocketEventDispatcher::ReceiveParams::ReceiveParams() {} | 97 BluetoothSocketEventDispatcher::SocketParams::SocketParams() {} |
| 82 | 98 |
| 83 BluetoothSocketEventDispatcher::ReceiveParams::~ReceiveParams() {} | 99 BluetoothSocketEventDispatcher::SocketParams::~SocketParams() {} |
| 84 | 100 |
| 85 void BluetoothSocketEventDispatcher::OnSocketConnect( | 101 void BluetoothSocketEventDispatcher::OnSocketConnect( |
| 86 const std::string& extension_id, | 102 const std::string& extension_id, |
| 87 int socket_id) { | 103 int socket_id) { |
| 88 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); | 104 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); |
| 89 | 105 |
| 90 StartSocketReceive(extension_id, socket_id); | 106 SocketParams params; |
| 91 } | |
| 92 | |
| 93 void BluetoothSocketEventDispatcher::OnSocketResume( | |
| 94 const std::string& extension_id, | |
| 95 int socket_id) { | |
| 96 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); | |
| 97 | |
| 98 StartSocketReceive(extension_id, socket_id); | |
| 99 } | |
| 100 | |
| 101 void BluetoothSocketEventDispatcher::StartSocketReceive( | |
| 102 const std::string& extension_id, | |
| 103 int socket_id) { | |
| 104 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); | |
| 105 | |
| 106 ReceiveParams params; | |
| 107 params.thread_id = thread_id_; | 107 params.thread_id = thread_id_; |
| 108 params.browser_context_id = browser_context_; | 108 params.browser_context_id = browser_context_; |
| 109 params.extension_id = extension_id; | 109 params.extension_id = extension_id; |
| 110 params.sockets = sockets_; | 110 params.sockets = sockets_; |
| 111 params.socket_id = socket_id; | 111 params.socket_id = socket_id; |
| 112 | 112 |
| 113 StartReceive(params); | 113 StartReceive(params); |
| 114 } | 114 } |
| 115 | 115 |
| 116 void BluetoothSocketEventDispatcher::OnSocketListen( | |
| 117 const std::string& extension_id, | |
| 118 int socket_id) { | |
| 119 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); | |
| 120 | |
| 121 SocketParams params; | |
| 122 params.thread_id = thread_id_; | |
| 123 params.browser_context_id = browser_context_; | |
| 124 params.extension_id = extension_id; | |
| 125 params.sockets = sockets_; | |
| 126 params.socket_id = socket_id; | |
| 127 | |
| 128 StartAccept(params); | |
| 129 } | |
| 130 | |
| 131 void BluetoothSocketEventDispatcher::OnSocketResume( | |
| 132 const std::string& extension_id, | |
| 133 int socket_id) { | |
| 134 DCHECK(BrowserThread::CurrentlyOn(thread_id_)); | |
| 135 | |
| 136 SocketParams params; | |
| 137 params.thread_id = thread_id_; | |
| 138 params.browser_context_id = browser_context_; | |
| 139 params.extension_id = extension_id; | |
| 140 params.sockets = sockets_; | |
| 141 params.socket_id = socket_id; | |
| 142 | |
| 143 BluetoothApiSocket* socket = | |
| 144 params.sockets->Get(params.extension_id, params.socket_id); | |
| 145 if (!socket) { | |
| 146 // This can happen if the socket is closed while our callback is active. | |
| 147 return; | |
| 148 } else if (socket->IsConnected()) { | |
|
armansito
2014/05/08 22:00:16
nit: No need for else since the previous block ret
| |
| 149 StartReceive(params); | |
| 150 } else { | |
| 151 StartAccept(params); | |
| 152 } | |
| 153 } | |
| 154 | |
| 116 // static | 155 // static |
| 117 void BluetoothSocketEventDispatcher::StartReceive(const ReceiveParams& params) { | 156 void BluetoothSocketEventDispatcher::StartReceive(const SocketParams& params) { |
| 118 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | 157 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); |
| 119 | 158 |
| 120 BluetoothApiSocket* socket = | 159 BluetoothApiSocket* socket = |
| 121 params.sockets->Get(params.extension_id, params.socket_id); | 160 params.sockets->Get(params.extension_id, params.socket_id); |
| 122 if (!socket) { | 161 if (!socket) { |
| 123 // This can happen if the socket is closed while our callback is active. | 162 // This can happen if the socket is closed while our callback is active. |
| 124 return; | 163 return; |
| 125 } | 164 } |
| 126 DCHECK(params.extension_id == socket->owner_extension_id()) | 165 DCHECK(params.extension_id == socket->owner_extension_id()) |
| 127 << "Socket has wrong owner."; | 166 << "Socket has wrong owner."; |
| 128 | 167 |
| 129 // Don't start another read if the socket has been paused. | 168 // Don't start another read if the socket has been paused. |
| 130 if (socket->paused()) | 169 if (socket->paused()) |
| 131 return; | 170 return; |
| 132 | 171 |
| 133 int buffer_size = socket->buffer_size(); | 172 int buffer_size = socket->buffer_size(); |
| 134 if (buffer_size <= 0) | 173 if (buffer_size <= 0) |
| 135 buffer_size = kDefaultBufferSize; | 174 buffer_size = kDefaultBufferSize; |
| 136 socket->Receive( | 175 socket->Receive( |
| 137 buffer_size, | 176 buffer_size, |
| 138 base::Bind( | 177 base::Bind( |
| 139 &BluetoothSocketEventDispatcher::ReceiveCallback, params), | 178 &BluetoothSocketEventDispatcher::ReceiveCallback, params), |
| 140 base::Bind( | 179 base::Bind( |
| 141 &BluetoothSocketEventDispatcher::ReceiveErrorCallback, params)); | 180 &BluetoothSocketEventDispatcher::ReceiveErrorCallback, params)); |
| 142 } | 181 } |
| 143 | 182 |
| 144 // static | 183 // static |
| 145 void BluetoothSocketEventDispatcher::ReceiveCallback( | 184 void BluetoothSocketEventDispatcher::ReceiveCallback( |
| 146 const ReceiveParams& params, | 185 const SocketParams& params, |
| 147 int bytes_read, | 186 int bytes_read, |
| 148 scoped_refptr<net::IOBuffer> io_buffer) { | 187 scoped_refptr<net::IOBuffer> io_buffer) { |
| 149 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | 188 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); |
| 150 | 189 |
| 151 // Dispatch "onReceive" event. | 190 // Dispatch "onReceive" event. |
| 152 bluetooth_socket::ReceiveInfo receive_info; | 191 bluetooth_socket::ReceiveInfo receive_info; |
| 153 receive_info.socket_id = params.socket_id; | 192 receive_info.socket_id = params.socket_id; |
| 154 receive_info.data = std::string(io_buffer->data(), bytes_read); | 193 receive_info.data = std::string(io_buffer->data(), bytes_read); |
| 155 scoped_ptr<base::ListValue> args = | 194 scoped_ptr<base::ListValue> args = |
| 156 bluetooth_socket::OnReceive::Create(receive_info); | 195 bluetooth_socket::OnReceive::Create(receive_info); |
| 157 scoped_ptr<Event> event( | 196 scoped_ptr<Event> event( |
| 158 new Event(bluetooth_socket::OnReceive::kEventName, args.Pass())); | 197 new Event(bluetooth_socket::OnReceive::kEventName, args.Pass())); |
| 159 PostEvent(params, event.Pass()); | 198 PostEvent(params, event.Pass()); |
| 160 | 199 |
| 161 // Post a task to delay the read until the socket is available, as | 200 // Post a task to delay the read until the socket is available, as |
| 162 // calling StartReceive at this point would error with ERR_IO_PENDING. | 201 // calling StartReceive at this point would error with ERR_IO_PENDING. |
| 163 BrowserThread::PostTask( | 202 BrowserThread::PostTask( |
| 164 params.thread_id, | 203 params.thread_id, |
| 165 FROM_HERE, | 204 FROM_HERE, |
| 166 base::Bind(&BluetoothSocketEventDispatcher::StartReceive, params)); | 205 base::Bind(&BluetoothSocketEventDispatcher::StartReceive, params)); |
| 167 } | 206 } |
| 168 | 207 |
| 169 // static | 208 // static |
| 170 void BluetoothSocketEventDispatcher::ReceiveErrorCallback( | 209 void BluetoothSocketEventDispatcher::ReceiveErrorCallback( |
| 171 const ReceiveParams& params, | 210 const SocketParams& params, |
| 172 BluetoothApiSocket::ErrorReason error_reason, | 211 BluetoothApiSocket::ErrorReason error_reason, |
| 173 const std::string& error) { | 212 const std::string& error) { |
| 174 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | 213 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); |
| 175 | 214 |
| 176 if (error_reason == BluetoothApiSocket::kIOPending) { | 215 if (error_reason == BluetoothApiSocket::kIOPending) { |
| 177 // This happens when resuming a socket which already had an active "read" | 216 // This happens when resuming a socket which already had an active "read" |
| 178 // callback. We can safely ignore this error, as the application should not | 217 // callback. We can safely ignore this error, as the application should not |
| 179 // care. | 218 // care. |
| 180 return; | 219 return; |
| 181 } | 220 } |
| 182 | 221 |
| 183 // Dispatch "onReceiveError" event but don't start another read to avoid | 222 // Dispatch "onReceiveError" event but don't start another read to avoid |
| 184 // potential infinite reads if we have a persistent network error. | 223 // potential infinite reads if we have a persistent network error. |
| 185 bluetooth_socket::ReceiveErrorInfo receive_error_info; | 224 bluetooth_socket::ReceiveErrorInfo receive_error_info; |
| 186 receive_error_info.socket_id = params.socket_id; | 225 receive_error_info.socket_id = params.socket_id; |
| 187 receive_error_info.error_message = error; | 226 receive_error_info.error_message = error; |
| 188 receive_error_info.error = MapErrorReason(error_reason); | 227 receive_error_info.error = MapReceiveErrorReason(error_reason); |
| 189 scoped_ptr<base::ListValue> args = | 228 scoped_ptr<base::ListValue> args = |
| 190 bluetooth_socket::OnReceiveError::Create(receive_error_info); | 229 bluetooth_socket::OnReceiveError::Create(receive_error_info); |
| 191 scoped_ptr<Event> event( | 230 scoped_ptr<Event> event( |
| 192 new Event(bluetooth_socket::OnReceiveError::kEventName, args.Pass())); | 231 new Event(bluetooth_socket::OnReceiveError::kEventName, args.Pass())); |
| 193 PostEvent(params, event.Pass()); | 232 PostEvent(params, event.Pass()); |
| 194 | 233 |
| 195 // Since we got an error, the socket is now "paused" until the application | 234 // Since we got an error, the socket is now "paused" until the application |
| 196 // "resumes" it. | 235 // "resumes" it. |
| 197 BluetoothApiSocket* socket = | 236 BluetoothApiSocket* socket = |
| 198 params.sockets->Get(params.extension_id, params.socket_id); | 237 params.sockets->Get(params.extension_id, params.socket_id); |
| 199 if (socket) { | 238 if (socket) { |
| 200 socket->set_paused(true); | 239 socket->set_paused(true); |
| 201 } | 240 } |
| 202 } | 241 } |
| 203 | 242 |
| 204 // static | 243 // static |
| 205 void BluetoothSocketEventDispatcher::PostEvent(const ReceiveParams& params, | 244 void BluetoothSocketEventDispatcher::StartAccept(const SocketParams& params) { |
| 245 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | |
| 246 | |
| 247 BluetoothApiSocket* socket = | |
| 248 params.sockets->Get(params.extension_id, params.socket_id); | |
| 249 if (!socket) { | |
| 250 // This can happen if the socket is closed while our callback is active. | |
| 251 return; | |
| 252 } | |
| 253 DCHECK(params.extension_id == socket->owner_extension_id()) | |
| 254 << "Socket has wrong owner."; | |
| 255 | |
| 256 // Don't start another accept if the socket has been paused. | |
| 257 if (socket->paused()) | |
| 258 return; | |
| 259 | |
| 260 socket->Accept( | |
| 261 base::Bind( | |
| 262 &BluetoothSocketEventDispatcher::AcceptCallback, params), | |
| 263 base::Bind( | |
| 264 &BluetoothSocketEventDispatcher::AcceptErrorCallback, params)); | |
| 265 } | |
| 266 | |
| 267 // static | |
| 268 void BluetoothSocketEventDispatcher::AcceptCallback( | |
| 269 const SocketParams& params, | |
| 270 const device::BluetoothDevice* device, | |
| 271 scoped_refptr<device::BluetoothSocket> socket) { | |
| 272 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | |
| 273 | |
| 274 BluetoothApiSocket* server_api_socket = | |
| 275 params.sockets->Get(params.extension_id, params.socket_id); | |
| 276 DCHECK(server_api_socket); | |
| 277 | |
| 278 BluetoothApiSocket* client_api_socket = new BluetoothApiSocket( | |
| 279 params.extension_id, | |
| 280 socket, | |
| 281 device->GetAddress(), | |
| 282 server_api_socket->uuid()); | |
| 283 int client_socket_id = params.sockets->Add(client_api_socket); | |
| 284 | |
| 285 // Dispatch "onAccept" event. | |
| 286 bluetooth_socket::AcceptInfo accept_info; | |
| 287 accept_info.socket_id = params.socket_id; | |
| 288 accept_info.client_socket_id = client_socket_id; | |
| 289 scoped_ptr<base::ListValue> args = | |
| 290 bluetooth_socket::OnAccept::Create(accept_info); | |
| 291 scoped_ptr<Event> event( | |
| 292 new Event(bluetooth_socket::OnAccept::kEventName, args.Pass())); | |
| 293 PostEvent(params, event.Pass()); | |
| 294 | |
| 295 // Post a task to delay the accept until the socket is available, as | |
| 296 // calling StartAccept at this point would error with ERR_IO_PENDING. | |
| 297 BrowserThread::PostTask( | |
| 298 params.thread_id, | |
| 299 FROM_HERE, | |
| 300 base::Bind(&BluetoothSocketEventDispatcher::StartAccept, params)); | |
| 301 } | |
| 302 | |
| 303 // static | |
| 304 void BluetoothSocketEventDispatcher::AcceptErrorCallback( | |
| 305 const SocketParams& params, | |
| 306 BluetoothApiSocket::ErrorReason error_reason, | |
| 307 const std::string& error) { | |
| 308 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | |
| 309 | |
| 310 if (error_reason == BluetoothApiSocket::kIOPending) { | |
| 311 // This happens when resuming a socket which already had an active "acce0t" | |
|
rpaquay
2014/05/09 16:09:53
accept?
keybuk
2014/05/09 18:28:24
Done.
| |
| 312 // callback. We can safely ignore this error, as the application should not | |
| 313 // care. | |
| 314 return; | |
| 315 } | |
| 316 | |
| 317 // Dispatch "onAcceptError" event but don't start another read to avoid | |
|
rpaquay
2014/05/09 16:09:53
read => accept
keybuk
2014/05/09 18:28:24
Done.
| |
| 318 // potential infinite reads if we have a persistent network error. | |
|
rpaquay
2014/05/09 16:09:53
reads => accepts
keybuk
2014/05/09 18:28:24
Done.
| |
| 319 bluetooth_socket::AcceptErrorInfo accept_error_info; | |
| 320 accept_error_info.socket_id = params.socket_id; | |
| 321 accept_error_info.error_message = error; | |
| 322 accept_error_info.error = MapAcceptErrorReason(error_reason); | |
| 323 scoped_ptr<base::ListValue> args = | |
| 324 bluetooth_socket::OnAcceptError::Create(accept_error_info); | |
| 325 scoped_ptr<Event> event( | |
| 326 new Event(bluetooth_socket::OnAcceptError::kEventName, args.Pass())); | |
| 327 PostEvent(params, event.Pass()); | |
| 328 | |
| 329 // Since we got an error, the socket is now "paused" until the application | |
| 330 // "resumes" it. | |
| 331 BluetoothApiSocket* socket = | |
| 332 params.sockets->Get(params.extension_id, params.socket_id); | |
| 333 if (socket) { | |
| 334 socket->set_paused(true); | |
| 335 } | |
| 336 } | |
| 337 | |
| 338 // static | |
| 339 void BluetoothSocketEventDispatcher::PostEvent(const SocketParams& params, | |
| 206 scoped_ptr<Event> event) { | 340 scoped_ptr<Event> event) { |
| 207 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); | 341 DCHECK(BrowserThread::CurrentlyOn(params.thread_id)); |
| 208 | 342 |
| 209 BrowserThread::PostTask( | 343 BrowserThread::PostTask( |
| 210 BrowserThread::UI, | 344 BrowserThread::UI, |
| 211 FROM_HERE, | 345 FROM_HERE, |
| 212 base::Bind(&DispatchEvent, | 346 base::Bind(&DispatchEvent, |
| 213 params.browser_context_id, | 347 params.browser_context_id, |
| 214 params.extension_id, | 348 params.extension_id, |
| 215 base::Passed(event.Pass()))); | 349 base::Passed(event.Pass()))); |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 227 if (!extensions::ExtensionsBrowserClient::Get()->IsValidContext(context)) | 361 if (!extensions::ExtensionsBrowserClient::Get()->IsValidContext(context)) |
| 228 return; | 362 return; |
| 229 | 363 |
| 230 EventRouter* router = EventRouter::Get(context); | 364 EventRouter* router = EventRouter::Get(context); |
| 231 if (router) | 365 if (router) |
| 232 router->DispatchEventToExtension(extension_id, event.Pass()); | 366 router->DispatchEventToExtension(extension_id, event.Pass()); |
| 233 } | 367 } |
| 234 | 368 |
| 235 } // namespace api | 369 } // namespace api |
| 236 } // namespace extensions | 370 } // namespace extensions |
| OLD | NEW |