OLD | NEW |
(Empty) | |
| 1 /* |
| 2 * Copyright 2015 The WebRTC Project Authors. All rights reserved. |
| 3 * |
| 4 * Use of this source code is governed by a BSD-style license |
| 5 * that can be found in the LICENSE file in the root of the source |
| 6 * tree. An additional intellectual property rights grant can be found |
| 7 * in the file PATENTS. All contributing project authors may |
| 8 * be found in the AUTHORS file in the root of the source tree. |
| 9 */ |
| 10 |
| 11 #include "webrtc/p2p/base/transportcontroller.h" |
| 12 |
| 13 #include "webrtc/base/bind.h" |
| 14 #include "webrtc/base/checks.h" |
| 15 #include "webrtc/base/thread.h" |
| 16 #include "webrtc/p2p/base/dtlstransport.h" |
| 17 #include "webrtc/p2p/base/p2ptransport.h" |
| 18 |
| 19 namespace cricket { |
| 20 |
| 21 enum { |
| 22 MSG_ICECONNECTIONSTATE, |
| 23 MSG_RECEIVING, |
| 24 MSG_ICEGATHERINGSTATE, |
| 25 MSG_CANDIDATESGATHERED, |
| 26 }; |
| 27 |
| 28 struct CandidatesData : public rtc::MessageData { |
| 29 CandidatesData(const std::string& transport_name, |
| 30 const Candidates& candidates) |
| 31 : transport_name(transport_name), candidates(candidates) {} |
| 32 |
| 33 std::string transport_name; |
| 34 Candidates candidates; |
| 35 }; |
| 36 |
| 37 TransportController::TransportController(rtc::Thread* signaling_thread, |
| 38 rtc::Thread* worker_thread, |
| 39 PortAllocator* port_allocator) |
| 40 : signaling_thread_(signaling_thread), |
| 41 worker_thread_(worker_thread), |
| 42 port_allocator_(port_allocator) {} |
| 43 |
| 44 TransportController::~TransportController() { |
| 45 worker_thread_->Invoke<void>( |
| 46 rtc::Bind(&TransportController::DestroyAllTransports_w, this)); |
| 47 signaling_thread_->Clear(this); |
| 48 } |
| 49 |
| 50 bool TransportController::SetSslMaxProtocolVersion( |
| 51 rtc::SSLProtocolVersion version) { |
| 52 return worker_thread_->Invoke<bool>(rtc::Bind( |
| 53 &TransportController::SetSslMaxProtocolVersion_w, this, version)); |
| 54 } |
| 55 |
| 56 void TransportController::SetIceConnectionReceivingTimeout(int timeout_ms) { |
| 57 worker_thread_->Invoke<void>( |
| 58 rtc::Bind(&TransportController::SetIceConnectionReceivingTimeout_w, this, |
| 59 timeout_ms)); |
| 60 } |
| 61 |
| 62 void TransportController::SetIceRole(IceRole ice_role) { |
| 63 worker_thread_->Invoke<void>( |
| 64 rtc::Bind(&TransportController::SetIceRole_w, this, ice_role)); |
| 65 } |
| 66 |
| 67 bool TransportController::GetSslRole(rtc::SSLRole* role) { |
| 68 return worker_thread_->Invoke<bool>( |
| 69 rtc::Bind(&TransportController::GetSslRole_w, this, role)); |
| 70 } |
| 71 |
| 72 bool TransportController::SetLocalCertificate( |
| 73 const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) { |
| 74 return worker_thread_->Invoke<bool>(rtc::Bind( |
| 75 &TransportController::SetLocalCertificate_w, this, certificate)); |
| 76 } |
| 77 |
| 78 bool TransportController::GetLocalCertificate( |
| 79 const std::string& transport_name, |
| 80 rtc::scoped_refptr<rtc::RTCCertificate>* certificate) { |
| 81 return worker_thread_->Invoke<bool>( |
| 82 rtc::Bind(&TransportController::GetLocalCertificate_w, this, |
| 83 transport_name, certificate)); |
| 84 } |
| 85 |
| 86 bool TransportController::GetRemoteSSLCertificate( |
| 87 const std::string& transport_name, |
| 88 rtc::SSLCertificate** cert) { |
| 89 return worker_thread_->Invoke<bool>( |
| 90 rtc::Bind(&TransportController::GetRemoteSSLCertificate_w, this, |
| 91 transport_name, cert)); |
| 92 } |
| 93 |
| 94 bool TransportController::SetLocalTransportDescription( |
| 95 const std::string& transport_name, |
| 96 const TransportDescription& tdesc, |
| 97 ContentAction action, |
| 98 std::string* err) { |
| 99 return worker_thread_->Invoke<bool>( |
| 100 rtc::Bind(&TransportController::SetLocalTransportDescription_w, this, |
| 101 transport_name, tdesc, action, err)); |
| 102 } |
| 103 |
| 104 bool TransportController::SetRemoteTransportDescription( |
| 105 const std::string& transport_name, |
| 106 const TransportDescription& tdesc, |
| 107 ContentAction action, |
| 108 std::string* err) { |
| 109 return worker_thread_->Invoke<bool>( |
| 110 rtc::Bind(&TransportController::SetRemoteTransportDescription_w, this, |
| 111 transport_name, tdesc, action, err)); |
| 112 } |
| 113 |
| 114 void TransportController::MaybeStartGathering() { |
| 115 worker_thread_->Invoke<void>( |
| 116 rtc::Bind(&TransportController::MaybeStartGathering_w, this)); |
| 117 } |
| 118 |
| 119 bool TransportController::AddRemoteCandidates(const std::string& transport_name, |
| 120 const Candidates& candidates, |
| 121 std::string* err) { |
| 122 return worker_thread_->Invoke<bool>( |
| 123 rtc::Bind(&TransportController::AddRemoteCandidates_w, this, |
| 124 transport_name, candidates, err)); |
| 125 } |
| 126 |
| 127 bool TransportController::ReadyForRemoteCandidates( |
| 128 const std::string& transport_name) { |
| 129 return worker_thread_->Invoke<bool>(rtc::Bind( |
| 130 &TransportController::ReadyForRemoteCandidates_w, this, transport_name)); |
| 131 } |
| 132 |
| 133 bool TransportController::GetStats(const std::string& transport_name, |
| 134 TransportStats* stats) { |
| 135 return worker_thread_->Invoke<bool>( |
| 136 rtc::Bind(&TransportController::GetStats_w, this, transport_name, stats)); |
| 137 } |
| 138 |
| 139 TransportChannel* TransportController::CreateTransportChannel_w( |
| 140 const std::string& transport_name, |
| 141 int component) { |
| 142 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 143 |
| 144 Transport* transport = GetOrCreateTransport_w(transport_name); |
| 145 return transport->CreateChannel(component); |
| 146 } |
| 147 |
| 148 void TransportController::DestroyTransportChannel_w( |
| 149 const std::string& transport_name, |
| 150 int component) { |
| 151 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 152 |
| 153 Transport* transport = GetTransport_w(transport_name); |
| 154 if (!transport) { |
| 155 ASSERT(false); |
| 156 return; |
| 157 } |
| 158 transport->DestroyChannel(component); |
| 159 |
| 160 // Just as we create a Transport when its first channel is created, |
| 161 // we delete it when its last channel is deleted. |
| 162 if (!transport->HasChannels()) { |
| 163 DestroyTransport_w(transport_name); |
| 164 } |
| 165 } |
| 166 |
| 167 const rtc::scoped_refptr<rtc::RTCCertificate>& |
| 168 TransportController::certificate_for_testing() { |
| 169 return certificate_; |
| 170 } |
| 171 |
| 172 Transport* TransportController::CreateTransport_w( |
| 173 const std::string& transport_name) { |
| 174 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 175 |
| 176 Transport* transport = new DtlsTransport<P2PTransport>( |
| 177 transport_name, port_allocator(), certificate_); |
| 178 return transport; |
| 179 } |
| 180 |
| 181 Transport* TransportController::GetTransport_w( |
| 182 const std::string& transport_name) { |
| 183 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 184 |
| 185 auto iter = transports_.find(transport_name); |
| 186 return (iter != transports_.end()) ? iter->second : nullptr; |
| 187 } |
| 188 |
| 189 void TransportController::OnMessage(rtc::Message* pmsg) { |
| 190 RTC_DCHECK(signaling_thread_->IsCurrent()); |
| 191 |
| 192 switch (pmsg->message_id) { |
| 193 case MSG_ICECONNECTIONSTATE: { |
| 194 rtc::TypedMessageData<IceConnectionState>* data = |
| 195 static_cast<rtc::TypedMessageData<IceConnectionState>*>(pmsg->pdata); |
| 196 SignalConnectionState(data->data()); |
| 197 delete data; |
| 198 break; |
| 199 } |
| 200 case MSG_RECEIVING: { |
| 201 rtc::TypedMessageData<bool>* data = |
| 202 static_cast<rtc::TypedMessageData<bool>*>(pmsg->pdata); |
| 203 SignalReceiving(data->data()); |
| 204 delete data; |
| 205 break; |
| 206 } |
| 207 case MSG_ICEGATHERINGSTATE: { |
| 208 rtc::TypedMessageData<IceGatheringState>* data = |
| 209 static_cast<rtc::TypedMessageData<IceGatheringState>*>(pmsg->pdata); |
| 210 SignalGatheringState(data->data()); |
| 211 delete data; |
| 212 break; |
| 213 } |
| 214 case MSG_CANDIDATESGATHERED: { |
| 215 CandidatesData* data = static_cast<CandidatesData*>(pmsg->pdata); |
| 216 SignalCandidatesGathered(data->transport_name, data->candidates); |
| 217 delete data; |
| 218 break; |
| 219 } |
| 220 default: |
| 221 ASSERT(false); |
| 222 } |
| 223 } |
| 224 |
| 225 Transport* TransportController::GetOrCreateTransport_w( |
| 226 const std::string& transport_name) { |
| 227 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 228 |
| 229 Transport* transport = GetTransport_w(transport_name); |
| 230 if (transport) { |
| 231 return transport; |
| 232 } |
| 233 |
| 234 transport = CreateTransport_w(transport_name); |
| 235 // The stuff below happens outside of CreateTransport_w so that unit tests |
| 236 // can override CreateTransport_w to return a different type of transport. |
| 237 transport->SetSslMaxProtocolVersion(ssl_max_version_); |
| 238 transport->SetChannelReceivingTimeout(ice_receiving_timeout_ms_); |
| 239 transport->SetIceRole(ice_role_); |
| 240 transport->SetIceTiebreaker(ice_tiebreaker_); |
| 241 if (certificate_) { |
| 242 transport->SetLocalCertificate(certificate_); |
| 243 } |
| 244 transport->SignalConnecting.connect( |
| 245 this, &TransportController::OnTransportConnecting_w); |
| 246 transport->SignalWritableState.connect( |
| 247 this, &TransportController::OnTransportWritableState_w); |
| 248 transport->SignalReceivingState.connect( |
| 249 this, &TransportController::OnTransportReceivingState_w); |
| 250 transport->SignalCompleted.connect( |
| 251 this, &TransportController::OnTransportCompleted_w); |
| 252 transport->SignalFailed.connect(this, |
| 253 &TransportController::OnTransportFailed_w); |
| 254 transport->SignalGatheringState.connect( |
| 255 this, &TransportController::OnTransportGatheringState_w); |
| 256 transport->SignalCandidatesGathered.connect( |
| 257 this, &TransportController::OnTransportCandidatesGathered_w); |
| 258 transport->SignalRoleConflict.connect( |
| 259 this, &TransportController::OnTransportRoleConflict_w); |
| 260 transports_[transport_name] = transport; |
| 261 |
| 262 return transport; |
| 263 } |
| 264 |
| 265 void TransportController::DestroyTransport_w( |
| 266 const std::string& transport_name) { |
| 267 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 268 |
| 269 auto iter = transports_.find(transport_name); |
| 270 if (iter != transports_.end()) { |
| 271 delete iter->second; |
| 272 transports_.erase(transport_name); |
| 273 } |
| 274 // Destroying a transport may cause aggregate state to change. |
| 275 UpdateAggregateStates_w(); |
| 276 } |
| 277 |
| 278 void TransportController::DestroyAllTransports_w() { |
| 279 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 280 |
| 281 for (const auto& kv : transports_) { |
| 282 delete kv.second; |
| 283 } |
| 284 transports_.clear(); |
| 285 } |
| 286 |
| 287 bool TransportController::SetSslMaxProtocolVersion_w( |
| 288 rtc::SSLProtocolVersion version) { |
| 289 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 290 |
| 291 // Max SSL version can only be set before transports are created. |
| 292 if (!transports_.empty()) { |
| 293 return false; |
| 294 } |
| 295 |
| 296 ssl_max_version_ = version; |
| 297 return true; |
| 298 } |
| 299 |
| 300 void TransportController::SetIceConnectionReceivingTimeout_w(int timeout_ms) { |
| 301 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 302 ice_receiving_timeout_ms_ = timeout_ms; |
| 303 for (const auto& kv : transports_) { |
| 304 kv.second->SetChannelReceivingTimeout(timeout_ms); |
| 305 } |
| 306 } |
| 307 |
| 308 void TransportController::SetIceRole_w(IceRole ice_role) { |
| 309 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 310 ice_role_ = ice_role; |
| 311 for (const auto& kv : transports_) { |
| 312 kv.second->SetIceRole(ice_role_); |
| 313 } |
| 314 } |
| 315 |
| 316 bool TransportController::GetSslRole_w(rtc::SSLRole* role) { |
| 317 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 318 |
| 319 if (transports_.empty()) { |
| 320 return false; |
| 321 } |
| 322 return transports_.begin()->second->GetSslRole(role); |
| 323 } |
| 324 |
| 325 bool TransportController::SetLocalCertificate_w( |
| 326 const rtc::scoped_refptr<rtc::RTCCertificate>& certificate) { |
| 327 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 328 |
| 329 if (certificate_) { |
| 330 return false; |
| 331 } |
| 332 if (!certificate) { |
| 333 return false; |
| 334 } |
| 335 certificate_ = certificate; |
| 336 |
| 337 for (const auto& kv : transports_) { |
| 338 kv.second->SetLocalCertificate(certificate_); |
| 339 } |
| 340 return true; |
| 341 } |
| 342 |
| 343 bool TransportController::GetLocalCertificate_w( |
| 344 const std::string& transport_name, |
| 345 rtc::scoped_refptr<rtc::RTCCertificate>* certificate) { |
| 346 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 347 |
| 348 Transport* t = GetTransport_w(transport_name); |
| 349 if (!t) { |
| 350 return false; |
| 351 } |
| 352 |
| 353 return t->GetLocalCertificate(certificate); |
| 354 } |
| 355 |
| 356 bool TransportController::GetRemoteSSLCertificate_w( |
| 357 const std::string& transport_name, |
| 358 rtc::SSLCertificate** cert) { |
| 359 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 360 |
| 361 Transport* t = GetTransport_w(transport_name); |
| 362 if (!t) { |
| 363 return false; |
| 364 } |
| 365 |
| 366 return t->GetRemoteSSLCertificate(cert); |
| 367 } |
| 368 |
| 369 bool TransportController::SetLocalTransportDescription_w( |
| 370 const std::string& transport_name, |
| 371 const TransportDescription& tdesc, |
| 372 ContentAction action, |
| 373 std::string* err) { |
| 374 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 375 |
| 376 Transport* transport = GetTransport_w(transport_name); |
| 377 if (!transport) { |
| 378 // If we didn't find a transport, that's not an error; |
| 379 // it could have been deleted as a result of bundling. |
| 380 // TODO(deadbeef): Make callers smarter so they won't attempt to set a |
| 381 // description on a deleted transport. |
| 382 return true; |
| 383 } |
| 384 |
| 385 return transport->SetLocalTransportDescription(tdesc, action, err); |
| 386 } |
| 387 |
| 388 bool TransportController::SetRemoteTransportDescription_w( |
| 389 const std::string& transport_name, |
| 390 const TransportDescription& tdesc, |
| 391 ContentAction action, |
| 392 std::string* err) { |
| 393 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 394 |
| 395 Transport* transport = GetTransport_w(transport_name); |
| 396 if (!transport) { |
| 397 // If we didn't find a transport, that's not an error; |
| 398 // it could have been deleted as a result of bundling. |
| 399 // TODO(deadbeef): Make callers smarter so they won't attempt to set a |
| 400 // description on a deleted transport. |
| 401 return true; |
| 402 } |
| 403 |
| 404 return transport->SetRemoteTransportDescription(tdesc, action, err); |
| 405 } |
| 406 |
| 407 void TransportController::MaybeStartGathering_w() { |
| 408 for (const auto& kv : transports_) { |
| 409 kv.second->MaybeStartGathering(); |
| 410 } |
| 411 } |
| 412 |
| 413 bool TransportController::AddRemoteCandidates_w( |
| 414 const std::string& transport_name, |
| 415 const Candidates& candidates, |
| 416 std::string* err) { |
| 417 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 418 |
| 419 Transport* transport = GetTransport_w(transport_name); |
| 420 if (!transport) { |
| 421 // If we didn't find a transport, that's not an error; |
| 422 // it could have been deleted as a result of bundling. |
| 423 return true; |
| 424 } |
| 425 |
| 426 return transport->AddRemoteCandidates(candidates, err); |
| 427 } |
| 428 |
| 429 bool TransportController::ReadyForRemoteCandidates_w( |
| 430 const std::string& transport_name) { |
| 431 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 432 |
| 433 Transport* transport = GetTransport_w(transport_name); |
| 434 if (!transport) { |
| 435 return false; |
| 436 } |
| 437 return transport->ready_for_remote_candidates(); |
| 438 } |
| 439 |
| 440 bool TransportController::GetStats_w(const std::string& transport_name, |
| 441 TransportStats* stats) { |
| 442 RTC_DCHECK(worker_thread()->IsCurrent()); |
| 443 |
| 444 Transport* transport = GetTransport_w(transport_name); |
| 445 if (!transport) { |
| 446 return false; |
| 447 } |
| 448 return transport->GetStats(stats); |
| 449 } |
| 450 |
| 451 void TransportController::OnTransportConnecting_w(Transport* transport) { |
| 452 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 453 UpdateAggregateStates_w(); |
| 454 } |
| 455 |
| 456 void TransportController::OnTransportWritableState_w(Transport* transport) { |
| 457 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 458 UpdateAggregateStates_w(); |
| 459 } |
| 460 |
| 461 void TransportController::OnTransportReceivingState_w(Transport* transport) { |
| 462 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 463 UpdateAggregateStates_w(); |
| 464 } |
| 465 |
| 466 void TransportController::OnTransportCompleted_w(Transport* transport) { |
| 467 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 468 UpdateAggregateStates_w(); |
| 469 } |
| 470 |
| 471 void TransportController::OnTransportFailed_w(Transport* transport) { |
| 472 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 473 UpdateAggregateStates_w(); |
| 474 } |
| 475 |
| 476 void TransportController::OnTransportGatheringState_w(Transport* transport) { |
| 477 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 478 UpdateAggregateStates_w(); |
| 479 } |
| 480 |
| 481 void TransportController::OnTransportCandidatesGathered_w( |
| 482 Transport* transport, |
| 483 const std::vector<Candidate>& candidates) { |
| 484 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 485 CandidatesData* data = new CandidatesData(transport->name(), candidates); |
| 486 signaling_thread_->Post(this, MSG_CANDIDATESGATHERED, data); |
| 487 } |
| 488 |
| 489 void TransportController::OnTransportRoleConflict_w() { |
| 490 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 491 |
| 492 if (ice_role_switch_) { |
| 493 LOG(LS_WARNING) << "Repeat of role conflict signal from Transport."; |
| 494 return; |
| 495 } |
| 496 |
| 497 ice_role_switch_ = true; |
| 498 IceRole reversed_role = (ice_role_ == ICEROLE_CONTROLLING) |
| 499 ? ICEROLE_CONTROLLED |
| 500 : ICEROLE_CONTROLLING; |
| 501 for (const auto& kv : transports_) { |
| 502 kv.second->SetIceRole(reversed_role); |
| 503 } |
| 504 } |
| 505 |
| 506 void TransportController::UpdateAggregateStates_w() { |
| 507 RTC_DCHECK(worker_thread_->IsCurrent()); |
| 508 |
| 509 IceConnectionState new_connection_state = kIceConnectionConnecting; |
| 510 IceGatheringState new_gathering_state = kIceGatheringNew; |
| 511 bool any_receiving = false; |
| 512 bool any_failed = false; |
| 513 bool all_connected = HasChannels_w(); |
| 514 bool all_completed = HasChannels_w(); |
| 515 bool any_gathering = false; |
| 516 bool all_done_gathering = HasChannels_w(); |
| 517 for (const auto& kv : transports_) { |
| 518 // Ignore transports without channels since they're about to be deleted, |
| 519 // and their state is meaningless. |
| 520 if (!kv.second->HasChannels()) { |
| 521 continue; |
| 522 } |
| 523 any_receiving = any_receiving || kv.second->any_channel_receiving(); |
| 524 any_failed = any_failed || kv.second->AnyChannelFailed(); |
| 525 all_connected = all_connected && kv.second->all_channels_writable(); |
| 526 all_completed = all_completed && kv.second->AllChannelsCompleted(); |
| 527 any_gathering = |
| 528 any_gathering || kv.second->gathering_state() != kIceGatheringNew; |
| 529 all_done_gathering = all_done_gathering && |
| 530 kv.second->gathering_state() == kIceGatheringComplete; |
| 531 } |
| 532 |
| 533 if (any_failed) { |
| 534 new_connection_state = kIceConnectionFailed; |
| 535 } else if (all_completed) { |
| 536 new_connection_state = kIceConnectionCompleted; |
| 537 } else if (all_connected) { |
| 538 new_connection_state = kIceConnectionConnected; |
| 539 } |
| 540 if (connection_state_ != new_connection_state) { |
| 541 connection_state_ = new_connection_state; |
| 542 signaling_thread_->Post( |
| 543 this, MSG_ICECONNECTIONSTATE, |
| 544 new rtc::TypedMessageData<IceConnectionState>(new_connection_state)); |
| 545 } |
| 546 |
| 547 if (receiving_ != any_receiving) { |
| 548 receiving_ = any_receiving; |
| 549 signaling_thread_->Post(this, MSG_RECEIVING, |
| 550 new rtc::TypedMessageData<bool>(any_receiving)); |
| 551 } |
| 552 |
| 553 if (all_done_gathering) { |
| 554 new_gathering_state = kIceGatheringComplete; |
| 555 } else if (any_gathering) { |
| 556 new_gathering_state = kIceGatheringGathering; |
| 557 } |
| 558 if (gathering_state_ != new_gathering_state) { |
| 559 gathering_state_ = new_gathering_state; |
| 560 signaling_thread_->Post( |
| 561 this, MSG_ICEGATHERINGSTATE, |
| 562 new rtc::TypedMessageData<IceGatheringState>(new_gathering_state)); |
| 563 } |
| 564 } |
| 565 |
| 566 bool TransportController::HasChannels_w() { |
| 567 for (const auto& kv : transports_) { |
| 568 if (kv.second->HasChannels()) { |
| 569 return true; |
| 570 } |
| 571 } |
| 572 return false; |
| 573 } |
| 574 |
| 575 } // namespace cricket |
OLD | NEW |