| OLD | NEW |
| (Empty) |
| 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 | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #include "device/bluetooth/bluetooth_remote_gatt_characteristic_chromeos.h" | |
| 6 | |
| 7 #include <limits> | |
| 8 | |
| 9 #include "base/logging.h" | |
| 10 #include "base/strings/stringprintf.h" | |
| 11 #include "device/bluetooth/bluetooth_adapter_chromeos.h" | |
| 12 #include "device/bluetooth/bluetooth_device.h" | |
| 13 #include "device/bluetooth/bluetooth_gatt_notify_session_chromeos.h" | |
| 14 #include "device/bluetooth/bluetooth_remote_gatt_characteristic_chromeos.h" | |
| 15 #include "device/bluetooth/bluetooth_remote_gatt_descriptor_chromeos.h" | |
| 16 #include "device/bluetooth/bluetooth_remote_gatt_service_chromeos.h" | |
| 17 #include "device/bluetooth/dbus/bluez_dbus_manager.h" | |
| 18 #include "third_party/cros_system_api/dbus/service_constants.h" | |
| 19 | |
| 20 namespace chromeos { | |
| 21 | |
| 22 namespace { | |
| 23 | |
| 24 // Stream operator for logging vector<uint8>. | |
| 25 std::ostream& operator<<(std::ostream& out, const std::vector<uint8> bytes) { | |
| 26 out << "["; | |
| 27 for (std::vector<uint8>::const_iterator iter = bytes.begin(); | |
| 28 iter != bytes.end(); ++iter) { | |
| 29 out << base::StringPrintf("%02X", *iter); | |
| 30 } | |
| 31 return out << "]"; | |
| 32 } | |
| 33 | |
| 34 } // namespace | |
| 35 | |
| 36 BluetoothRemoteGattCharacteristicChromeOS:: | |
| 37 BluetoothRemoteGattCharacteristicChromeOS( | |
| 38 BluetoothRemoteGattServiceChromeOS* service, | |
| 39 const dbus::ObjectPath& object_path) | |
| 40 : object_path_(object_path), | |
| 41 service_(service), | |
| 42 num_notify_sessions_(0), | |
| 43 notify_call_pending_(false), | |
| 44 weak_ptr_factory_(this) { | |
| 45 VLOG(1) << "Creating remote GATT characteristic with identifier: " | |
| 46 << GetIdentifier() << ", UUID: " << GetUUID().canonical_value(); | |
| 47 bluez::BluezDBusManager::Get() | |
| 48 ->GetBluetoothGattDescriptorClient() | |
| 49 ->AddObserver(this); | |
| 50 | |
| 51 // Add all known GATT characteristic descriptors. | |
| 52 const std::vector<dbus::ObjectPath>& gatt_descs = | |
| 53 bluez::BluezDBusManager::Get() | |
| 54 ->GetBluetoothGattDescriptorClient() | |
| 55 ->GetDescriptors(); | |
| 56 for (std::vector<dbus::ObjectPath>::const_iterator iter = gatt_descs.begin(); | |
| 57 iter != gatt_descs.end(); ++iter) | |
| 58 GattDescriptorAdded(*iter); | |
| 59 } | |
| 60 | |
| 61 BluetoothRemoteGattCharacteristicChromeOS:: | |
| 62 ~BluetoothRemoteGattCharacteristicChromeOS() { | |
| 63 bluez::BluezDBusManager::Get() | |
| 64 ->GetBluetoothGattDescriptorClient() | |
| 65 ->RemoveObserver(this); | |
| 66 | |
| 67 // Clean up all the descriptors. There isn't much point in notifying service | |
| 68 // observers for each descriptor that gets removed, so just delete them. | |
| 69 for (DescriptorMap::iterator iter = descriptors_.begin(); | |
| 70 iter != descriptors_.end(); ++iter) | |
| 71 delete iter->second; | |
| 72 | |
| 73 // Report an error for all pending calls to StartNotifySession. | |
| 74 while (!pending_start_notify_calls_.empty()) { | |
| 75 PendingStartNotifyCall callbacks = pending_start_notify_calls_.front(); | |
| 76 pending_start_notify_calls_.pop(); | |
| 77 callbacks.second.Run(device::BluetoothGattService::GATT_ERROR_FAILED); | |
| 78 } | |
| 79 } | |
| 80 | |
| 81 std::string BluetoothRemoteGattCharacteristicChromeOS::GetIdentifier() const { | |
| 82 return object_path_.value(); | |
| 83 } | |
| 84 | |
| 85 device::BluetoothUUID | |
| 86 BluetoothRemoteGattCharacteristicChromeOS::GetUUID() const { | |
| 87 bluez::BluetoothGattCharacteristicClient::Properties* properties = | |
| 88 bluez::BluezDBusManager::Get() | |
| 89 ->GetBluetoothGattCharacteristicClient() | |
| 90 ->GetProperties(object_path_); | |
| 91 DCHECK(properties); | |
| 92 return device::BluetoothUUID(properties->uuid.value()); | |
| 93 } | |
| 94 | |
| 95 bool BluetoothRemoteGattCharacteristicChromeOS::IsLocal() const { | |
| 96 return false; | |
| 97 } | |
| 98 | |
| 99 const std::vector<uint8>& | |
| 100 BluetoothRemoteGattCharacteristicChromeOS::GetValue() const { | |
| 101 bluez::BluetoothGattCharacteristicClient::Properties* properties = | |
| 102 bluez::BluezDBusManager::Get() | |
| 103 ->GetBluetoothGattCharacteristicClient() | |
| 104 ->GetProperties(object_path_); | |
| 105 | |
| 106 DCHECK(properties); | |
| 107 | |
| 108 return properties->value.value(); | |
| 109 } | |
| 110 | |
| 111 device::BluetoothGattService* | |
| 112 BluetoothRemoteGattCharacteristicChromeOS::GetService() const { | |
| 113 return service_; | |
| 114 } | |
| 115 | |
| 116 device::BluetoothGattCharacteristic::Properties | |
| 117 BluetoothRemoteGattCharacteristicChromeOS::GetProperties() const { | |
| 118 bluez::BluetoothGattCharacteristicClient::Properties* properties = | |
| 119 bluez::BluezDBusManager::Get() | |
| 120 ->GetBluetoothGattCharacteristicClient() | |
| 121 ->GetProperties(object_path_); | |
| 122 DCHECK(properties); | |
| 123 | |
| 124 Properties props = PROPERTY_NONE; | |
| 125 const std::vector<std::string>& flags = properties->flags.value(); | |
| 126 for (std::vector<std::string>::const_iterator iter = flags.begin(); | |
| 127 iter != flags.end(); | |
| 128 ++iter) { | |
| 129 if (*iter == bluetooth_gatt_characteristic::kFlagBroadcast) | |
| 130 props |= PROPERTY_BROADCAST; | |
| 131 if (*iter == bluetooth_gatt_characteristic::kFlagRead) | |
| 132 props |= PROPERTY_READ; | |
| 133 if (*iter == bluetooth_gatt_characteristic::kFlagWriteWithoutResponse) | |
| 134 props |= PROPERTY_WRITE_WITHOUT_RESPONSE; | |
| 135 if (*iter == bluetooth_gatt_characteristic::kFlagWrite) | |
| 136 props |= PROPERTY_WRITE; | |
| 137 if (*iter == bluetooth_gatt_characteristic::kFlagNotify) | |
| 138 props |= PROPERTY_NOTIFY; | |
| 139 if (*iter == bluetooth_gatt_characteristic::kFlagIndicate) | |
| 140 props |= PROPERTY_INDICATE; | |
| 141 if (*iter == bluetooth_gatt_characteristic::kFlagAuthenticatedSignedWrites) | |
| 142 props |= PROPERTY_AUTHENTICATED_SIGNED_WRITES; | |
| 143 if (*iter == bluetooth_gatt_characteristic::kFlagExtendedProperties) | |
| 144 props |= PROPERTY_EXTENDED_PROPERTIES; | |
| 145 if (*iter == bluetooth_gatt_characteristic::kFlagReliableWrite) | |
| 146 props |= PROPERTY_RELIABLE_WRITE; | |
| 147 if (*iter == bluetooth_gatt_characteristic::kFlagWritableAuxiliaries) | |
| 148 props |= PROPERTY_WRITABLE_AUXILIARIES; | |
| 149 } | |
| 150 | |
| 151 return props; | |
| 152 } | |
| 153 | |
| 154 device::BluetoothGattCharacteristic::Permissions | |
| 155 BluetoothRemoteGattCharacteristicChromeOS::GetPermissions() const { | |
| 156 // TODO(armansito): Once BlueZ defines the permissions, return the correct | |
| 157 // values here. | |
| 158 return PERMISSION_NONE; | |
| 159 } | |
| 160 | |
| 161 bool BluetoothRemoteGattCharacteristicChromeOS::IsNotifying() const { | |
| 162 bluez::BluetoothGattCharacteristicClient::Properties* properties = | |
| 163 bluez::BluezDBusManager::Get() | |
| 164 ->GetBluetoothGattCharacteristicClient() | |
| 165 ->GetProperties(object_path_); | |
| 166 DCHECK(properties); | |
| 167 | |
| 168 return properties->notifying.value(); | |
| 169 } | |
| 170 | |
| 171 std::vector<device::BluetoothGattDescriptor*> | |
| 172 BluetoothRemoteGattCharacteristicChromeOS::GetDescriptors() const { | |
| 173 std::vector<device::BluetoothGattDescriptor*> descriptors; | |
| 174 for (DescriptorMap::const_iterator iter = descriptors_.begin(); | |
| 175 iter != descriptors_.end(); ++iter) | |
| 176 descriptors.push_back(iter->second); | |
| 177 return descriptors; | |
| 178 } | |
| 179 | |
| 180 device::BluetoothGattDescriptor* | |
| 181 BluetoothRemoteGattCharacteristicChromeOS::GetDescriptor( | |
| 182 const std::string& identifier) const { | |
| 183 DescriptorMap::const_iterator iter = | |
| 184 descriptors_.find(dbus::ObjectPath(identifier)); | |
| 185 if (iter == descriptors_.end()) | |
| 186 return NULL; | |
| 187 return iter->second; | |
| 188 } | |
| 189 | |
| 190 bool BluetoothRemoteGattCharacteristicChromeOS::AddDescriptor( | |
| 191 device::BluetoothGattDescriptor* descriptor) { | |
| 192 VLOG(1) << "Descriptors cannot be added to a remote GATT characteristic."; | |
| 193 return false; | |
| 194 } | |
| 195 | |
| 196 bool BluetoothRemoteGattCharacteristicChromeOS::UpdateValue( | |
| 197 const std::vector<uint8>& value) { | |
| 198 VLOG(1) << "Cannot update the value of a remote GATT characteristic."; | |
| 199 return false; | |
| 200 } | |
| 201 | |
| 202 void BluetoothRemoteGattCharacteristicChromeOS::ReadRemoteCharacteristic( | |
| 203 const ValueCallback& callback, | |
| 204 const ErrorCallback& error_callback) { | |
| 205 VLOG(1) << "Sending GATT characteristic read request to characteristic: " | |
| 206 << GetIdentifier() << ", UUID: " << GetUUID().canonical_value() | |
| 207 << "."; | |
| 208 | |
| 209 bluez::BluezDBusManager::Get() | |
| 210 ->GetBluetoothGattCharacteristicClient() | |
| 211 ->ReadValue( | |
| 212 object_path_, callback, | |
| 213 base::Bind(&BluetoothRemoteGattCharacteristicChromeOS::OnError, | |
| 214 weak_ptr_factory_.GetWeakPtr(), error_callback)); | |
| 215 } | |
| 216 | |
| 217 void BluetoothRemoteGattCharacteristicChromeOS::WriteRemoteCharacteristic( | |
| 218 const std::vector<uint8>& new_value, | |
| 219 const base::Closure& callback, | |
| 220 const ErrorCallback& error_callback) { | |
| 221 VLOG(1) << "Sending GATT characteristic write request to characteristic: " | |
| 222 << GetIdentifier() << ", UUID: " << GetUUID().canonical_value() | |
| 223 << ", with value: " << new_value << "."; | |
| 224 | |
| 225 bluez::BluezDBusManager::Get() | |
| 226 ->GetBluetoothGattCharacteristicClient() | |
| 227 ->WriteValue( | |
| 228 object_path_, new_value, callback, | |
| 229 base::Bind(&BluetoothRemoteGattCharacteristicChromeOS::OnError, | |
| 230 weak_ptr_factory_.GetWeakPtr(), error_callback)); | |
| 231 } | |
| 232 | |
| 233 void BluetoothRemoteGattCharacteristicChromeOS::StartNotifySession( | |
| 234 const NotifySessionCallback& callback, | |
| 235 const ErrorCallback& error_callback) { | |
| 236 VLOG(1) << __func__; | |
| 237 | |
| 238 if (num_notify_sessions_ > 0) { | |
| 239 // The characteristic might have stopped notifying even though the session | |
| 240 // count is nonzero. This means that notifications stopped outside of our | |
| 241 // control and we should reset the count. If the characteristic is still | |
| 242 // notifying, then return success. Otherwise, reset the count and treat | |
| 243 // this call as if the count were 0. | |
| 244 if (IsNotifying()) { | |
| 245 // Check for overflows, though unlikely. | |
| 246 if (num_notify_sessions_ == std::numeric_limits<size_t>::max()) { | |
| 247 error_callback.Run(device::BluetoothGattService::GATT_ERROR_FAILED); | |
| 248 return; | |
| 249 } | |
| 250 | |
| 251 ++num_notify_sessions_; | |
| 252 DCHECK(service_); | |
| 253 DCHECK(service_->GetAdapter()); | |
| 254 DCHECK(service_->GetDevice()); | |
| 255 scoped_ptr<device::BluetoothGattNotifySession> session( | |
| 256 new BluetoothGattNotifySessionChromeOS( | |
| 257 service_->GetAdapter(), | |
| 258 service_->GetDevice()->GetAddress(), | |
| 259 service_->GetIdentifier(), | |
| 260 GetIdentifier(), | |
| 261 object_path_)); | |
| 262 callback.Run(session.Pass()); | |
| 263 return; | |
| 264 } | |
| 265 | |
| 266 num_notify_sessions_ = 0; | |
| 267 } | |
| 268 | |
| 269 // Queue the callbacks if there is a pending call to bluetoothd. | |
| 270 if (notify_call_pending_) { | |
| 271 pending_start_notify_calls_.push(std::make_pair(callback, error_callback)); | |
| 272 return; | |
| 273 } | |
| 274 | |
| 275 notify_call_pending_ = true; | |
| 276 bluez::BluezDBusManager::Get() | |
| 277 ->GetBluetoothGattCharacteristicClient() | |
| 278 ->StartNotify( | |
| 279 object_path_, | |
| 280 base::Bind( | |
| 281 &BluetoothRemoteGattCharacteristicChromeOS::OnStartNotifySuccess, | |
| 282 weak_ptr_factory_.GetWeakPtr(), callback), | |
| 283 base::Bind( | |
| 284 &BluetoothRemoteGattCharacteristicChromeOS::OnStartNotifyError, | |
| 285 weak_ptr_factory_.GetWeakPtr(), error_callback)); | |
| 286 } | |
| 287 | |
| 288 void BluetoothRemoteGattCharacteristicChromeOS::RemoveNotifySession( | |
| 289 const base::Closure& callback) { | |
| 290 VLOG(1) << __func__; | |
| 291 | |
| 292 if (num_notify_sessions_ > 1) { | |
| 293 DCHECK(!notify_call_pending_); | |
| 294 --num_notify_sessions_; | |
| 295 callback.Run(); | |
| 296 return; | |
| 297 } | |
| 298 | |
| 299 // Notifications may have stopped outside our control. If the characteristic | |
| 300 // is no longer notifying, return success. | |
| 301 if (!IsNotifying()) { | |
| 302 num_notify_sessions_ = 0; | |
| 303 callback.Run(); | |
| 304 return; | |
| 305 } | |
| 306 | |
| 307 if (notify_call_pending_ || num_notify_sessions_ == 0) { | |
| 308 callback.Run(); | |
| 309 return; | |
| 310 } | |
| 311 | |
| 312 DCHECK(num_notify_sessions_ == 1); | |
| 313 notify_call_pending_ = true; | |
| 314 bluez::BluezDBusManager::Get() | |
| 315 ->GetBluetoothGattCharacteristicClient() | |
| 316 ->StopNotify( | |
| 317 object_path_, | |
| 318 base::Bind( | |
| 319 &BluetoothRemoteGattCharacteristicChromeOS::OnStopNotifySuccess, | |
| 320 weak_ptr_factory_.GetWeakPtr(), callback), | |
| 321 base::Bind( | |
| 322 &BluetoothRemoteGattCharacteristicChromeOS::OnStopNotifyError, | |
| 323 weak_ptr_factory_.GetWeakPtr(), callback)); | |
| 324 } | |
| 325 | |
| 326 void BluetoothRemoteGattCharacteristicChromeOS::GattDescriptorAdded( | |
| 327 const dbus::ObjectPath& object_path) { | |
| 328 if (descriptors_.find(object_path) != descriptors_.end()) { | |
| 329 VLOG(1) << "Remote GATT characteristic descriptor already exists: " | |
| 330 << object_path.value(); | |
| 331 return; | |
| 332 } | |
| 333 | |
| 334 bluez::BluetoothGattDescriptorClient::Properties* properties = | |
| 335 bluez::BluezDBusManager::Get() | |
| 336 ->GetBluetoothGattDescriptorClient() | |
| 337 ->GetProperties(object_path); | |
| 338 DCHECK(properties); | |
| 339 if (properties->characteristic.value() != object_path_) { | |
| 340 VLOG(3) << "Remote GATT descriptor does not belong to this characteristic."; | |
| 341 return; | |
| 342 } | |
| 343 | |
| 344 VLOG(1) << "Adding new remote GATT descriptor for GATT characteristic: " | |
| 345 << GetIdentifier() << ", UUID: " << GetUUID().canonical_value(); | |
| 346 | |
| 347 BluetoothRemoteGattDescriptorChromeOS* descriptor = | |
| 348 new BluetoothRemoteGattDescriptorChromeOS(this, object_path); | |
| 349 descriptors_[object_path] = descriptor; | |
| 350 DCHECK(descriptor->GetIdentifier() == object_path.value()); | |
| 351 DCHECK(descriptor->GetUUID().IsValid()); | |
| 352 DCHECK(service_); | |
| 353 | |
| 354 service_->NotifyDescriptorAddedOrRemoved(this, descriptor, true /* added */); | |
| 355 } | |
| 356 | |
| 357 void BluetoothRemoteGattCharacteristicChromeOS::GattDescriptorRemoved( | |
| 358 const dbus::ObjectPath& object_path) { | |
| 359 DescriptorMap::iterator iter = descriptors_.find(object_path); | |
| 360 if (iter == descriptors_.end()) { | |
| 361 VLOG(2) << "Unknown descriptor removed: " << object_path.value(); | |
| 362 return; | |
| 363 } | |
| 364 | |
| 365 VLOG(1) << "Removing remote GATT descriptor from characteristic: " | |
| 366 << GetIdentifier() << ", UUID: " << GetUUID().canonical_value(); | |
| 367 | |
| 368 BluetoothRemoteGattDescriptorChromeOS* descriptor = iter->second; | |
| 369 DCHECK(descriptor->object_path() == object_path); | |
| 370 descriptors_.erase(iter); | |
| 371 | |
| 372 DCHECK(service_); | |
| 373 service_->NotifyDescriptorAddedOrRemoved(this, descriptor, false /* added */); | |
| 374 | |
| 375 delete descriptor; | |
| 376 } | |
| 377 | |
| 378 void BluetoothRemoteGattCharacteristicChromeOS::GattDescriptorPropertyChanged( | |
| 379 const dbus::ObjectPath& object_path, | |
| 380 const std::string& property_name) { | |
| 381 DescriptorMap::iterator iter = descriptors_.find(object_path); | |
| 382 if (iter == descriptors_.end()) { | |
| 383 VLOG(2) << "Unknown descriptor removed: " << object_path.value(); | |
| 384 return; | |
| 385 } | |
| 386 | |
| 387 bluez::BluetoothGattDescriptorClient::Properties* properties = | |
| 388 bluez::BluezDBusManager::Get() | |
| 389 ->GetBluetoothGattDescriptorClient() | |
| 390 ->GetProperties(object_path); | |
| 391 | |
| 392 DCHECK(properties); | |
| 393 | |
| 394 if (property_name != properties->value.name()) | |
| 395 return; | |
| 396 | |
| 397 DCHECK(service_); | |
| 398 service_->NotifyDescriptorValueChanged(this, iter->second, | |
| 399 properties->value.value()); | |
| 400 } | |
| 401 | |
| 402 void BluetoothRemoteGattCharacteristicChromeOS::OnError( | |
| 403 const ErrorCallback& error_callback, | |
| 404 const std::string& error_name, | |
| 405 const std::string& error_message) { | |
| 406 VLOG(1) << "Operation failed: " << error_name << ", message: " | |
| 407 << error_message; | |
| 408 error_callback.Run( | |
| 409 BluetoothRemoteGattServiceChromeOS::DBusErrorToServiceError(error_name)); | |
| 410 } | |
| 411 | |
| 412 void BluetoothRemoteGattCharacteristicChromeOS::OnStartNotifySuccess( | |
| 413 const NotifySessionCallback& callback) { | |
| 414 VLOG(1) << "Started notifications from characteristic: " | |
| 415 << object_path_.value(); | |
| 416 DCHECK(num_notify_sessions_ == 0); | |
| 417 DCHECK(notify_call_pending_); | |
| 418 | |
| 419 ++num_notify_sessions_; | |
| 420 notify_call_pending_ = false; | |
| 421 | |
| 422 // Invoke the queued callbacks for this operation. | |
| 423 DCHECK(service_); | |
| 424 DCHECK(service_->GetDevice()); | |
| 425 scoped_ptr<device::BluetoothGattNotifySession> session( | |
| 426 new BluetoothGattNotifySessionChromeOS( | |
| 427 service_->GetAdapter(), | |
| 428 service_->GetDevice()->GetAddress(), | |
| 429 service_->GetIdentifier(), | |
| 430 GetIdentifier(), | |
| 431 object_path_)); | |
| 432 callback.Run(session.Pass()); | |
| 433 | |
| 434 ProcessStartNotifyQueue(); | |
| 435 } | |
| 436 | |
| 437 void BluetoothRemoteGattCharacteristicChromeOS::OnStartNotifyError( | |
| 438 const ErrorCallback& error_callback, | |
| 439 const std::string& error_name, | |
| 440 const std::string& error_message) { | |
| 441 VLOG(1) << "Failed to start notifications from characteristic: " | |
| 442 << object_path_.value() << ": " << error_name << ", " | |
| 443 << error_message; | |
| 444 DCHECK(num_notify_sessions_ == 0); | |
| 445 DCHECK(notify_call_pending_); | |
| 446 | |
| 447 notify_call_pending_ = false; | |
| 448 | |
| 449 error_callback.Run( | |
| 450 BluetoothRemoteGattServiceChromeOS::DBusErrorToServiceError(error_name)); | |
| 451 | |
| 452 ProcessStartNotifyQueue(); | |
| 453 } | |
| 454 | |
| 455 void BluetoothRemoteGattCharacteristicChromeOS::OnStopNotifySuccess( | |
| 456 const base::Closure& callback) { | |
| 457 DCHECK(notify_call_pending_); | |
| 458 DCHECK(num_notify_sessions_ == 1); | |
| 459 | |
| 460 notify_call_pending_ = false; | |
| 461 --num_notify_sessions_; | |
| 462 callback.Run(); | |
| 463 | |
| 464 ProcessStartNotifyQueue(); | |
| 465 } | |
| 466 | |
| 467 void BluetoothRemoteGattCharacteristicChromeOS::OnStopNotifyError( | |
| 468 const base::Closure& callback, | |
| 469 const std::string& error_name, | |
| 470 const std::string& error_message) { | |
| 471 VLOG(1) << "Call to stop notifications failed for characteristic: " | |
| 472 << object_path_.value() << ": " << error_name << ", " | |
| 473 << error_message; | |
| 474 | |
| 475 // Since this is a best effort operation, treat this as success. | |
| 476 OnStopNotifySuccess(callback); | |
| 477 } | |
| 478 | |
| 479 void BluetoothRemoteGattCharacteristicChromeOS::ProcessStartNotifyQueue() { | |
| 480 while (!pending_start_notify_calls_.empty()) { | |
| 481 PendingStartNotifyCall callbacks = pending_start_notify_calls_.front(); | |
| 482 pending_start_notify_calls_.pop(); | |
| 483 StartNotifySession(callbacks.first, callbacks.second); | |
| 484 } | |
| 485 } | |
| 486 | |
| 487 } // namespace chromeos | |
| OLD | NEW |