MyHandler.h revision 9fbd6ae6b6d9f3eb791a3385df6fed3524531bd4
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17#ifndef MY_HANDLER_H_ 18 19#define MY_HANDLER_H_ 20 21#include "APacketSource.h" 22#include "ARTPConnection.h" 23#include "ARTSPConnection.h" 24#include "ASessionDescription.h" 25 26#include <ctype.h> 27 28#include <media/stagefright/foundation/ABuffer.h> 29#include <media/stagefright/foundation/ADebug.h> 30#include <media/stagefright/foundation/ALooper.h> 31#include <media/stagefright/foundation/AMessage.h> 32#include <media/stagefright/MediaErrors.h> 33 34#define USE_TCP_INTERLEAVED 0 35 36namespace android { 37 38static bool GetAttribute(const char *s, const char *key, AString *value) { 39 value->clear(); 40 41 size_t keyLen = strlen(key); 42 43 for (;;) { 44 while (isspace(*s)) { 45 ++s; 46 } 47 48 const char *colonPos = strchr(s, ';'); 49 50 size_t len = 51 (colonPos == NULL) ? strlen(s) : colonPos - s; 52 53 if (len >= keyLen + 1 && s[keyLen] == '=' && !strncmp(s, key, keyLen)) { 54 value->setTo(&s[keyLen + 1], len - keyLen - 1); 55 return true; 56 } 57 58 if (colonPos == NULL) { 59 return false; 60 } 61 62 s = colonPos + 1; 63 } 64} 65 66struct MyHandler : public AHandler { 67 MyHandler(const char *url, const sp<ALooper> &looper) 68 : mLooper(looper), 69 mNetLooper(new ALooper), 70 mConn(new ARTSPConnection), 71 mRTPConn(new ARTPConnection), 72 mSessionURL(url), 73 mSetupTracksSuccessful(false), 74 mSeekPending(false), 75 mFirstAccessUnit(true), 76 mFirstAccessUnitNTP(0), 77 mNumAccessUnitsReceived(0), 78 mCheckPending(false) { 79 80 mNetLooper->start(false /* runOnCallingThread */, 81 false /* canCallJava */, 82 PRIORITY_HIGHEST); 83 } 84 85 void connect(const sp<AMessage> &doneMsg) { 86 mDoneMsg = doneMsg; 87 88 mLooper->registerHandler(this); 89 mLooper->registerHandler(mConn); 90 (1 ? mNetLooper : mLooper)->registerHandler(mRTPConn); 91 92 sp<AMessage> notify = new AMessage('biny', id()); 93 mConn->observeBinaryData(notify); 94 95 sp<AMessage> reply = new AMessage('conn', id()); 96 mConn->connect(mSessionURL.c_str(), reply); 97 } 98 99 void disconnect(const sp<AMessage> &doneMsg) { 100 mDoneMsg = doneMsg; 101 102 (new AMessage('abor', id()))->post(); 103 } 104 105 void seek(int64_t timeUs) { 106 sp<AMessage> msg = new AMessage('seek', id()); 107 msg->setInt64("time", timeUs); 108 msg->post(); 109 } 110 111 int64_t getNormalPlayTimeUs() { 112 int64_t maxTimeUs = 0; 113 for (size_t i = 0; i < mTracks.size(); ++i) { 114 int64_t timeUs = mTracks.editItemAt(i).mPacketSource 115 ->getNormalPlayTimeUs(); 116 117 if (i == 0 || timeUs > maxTimeUs) { 118 maxTimeUs = timeUs; 119 } 120 } 121 122 return maxTimeUs; 123 } 124 125 virtual void onMessageReceived(const sp<AMessage> &msg) { 126 switch (msg->what()) { 127 case 'conn': 128 { 129 int32_t result; 130 CHECK(msg->findInt32("result", &result)); 131 132 LOG(INFO) << "connection request completed with result " 133 << result << " (" << strerror(-result) << ")"; 134 135 if (result == OK) { 136 AString request; 137 request = "DESCRIBE "; 138 request.append(mSessionURL); 139 request.append(" RTSP/1.0\r\n"); 140 request.append("Accept: application/sdp\r\n"); 141 request.append("\r\n"); 142 143 sp<AMessage> reply = new AMessage('desc', id()); 144 mConn->sendRequest(request.c_str(), reply); 145 } else { 146 (new AMessage('disc', id()))->post(); 147 } 148 break; 149 } 150 151 case 'disc': 152 { 153 (new AMessage('quit', id()))->post(); 154 break; 155 } 156 157 case 'desc': 158 { 159 int32_t result; 160 CHECK(msg->findInt32("result", &result)); 161 162 LOG(INFO) << "DESCRIBE completed with result " 163 << result << " (" << strerror(-result) << ")"; 164 165 if (result == OK) { 166 sp<RefBase> obj; 167 CHECK(msg->findObject("response", &obj)); 168 sp<ARTSPResponse> response = 169 static_cast<ARTSPResponse *>(obj.get()); 170 171 if (response->mStatusCode == 302) { 172 ssize_t i = response->mHeaders.indexOfKey("location"); 173 CHECK_GE(i, 0); 174 175 mSessionURL = response->mHeaders.valueAt(i); 176 177 AString request; 178 request = "DESCRIBE "; 179 request.append(mSessionURL); 180 request.append(" RTSP/1.0\r\n"); 181 request.append("Accept: application/sdp\r\n"); 182 request.append("\r\n"); 183 184 sp<AMessage> reply = new AMessage('desc', id()); 185 mConn->sendRequest(request.c_str(), reply); 186 break; 187 } 188 189 CHECK_EQ(response->mStatusCode, 200u); 190 191 mSessionDesc = new ASessionDescription; 192 193 mSessionDesc->setTo( 194 response->mContent->data(), 195 response->mContent->size()); 196 197 CHECK(mSessionDesc->isValid()); 198 199 ssize_t i = response->mHeaders.indexOfKey("content-base"); 200 if (i >= 0) { 201 mBaseURL = response->mHeaders.valueAt(i); 202 } else { 203 i = response->mHeaders.indexOfKey("content-location"); 204 if (i >= 0) { 205 mBaseURL = response->mHeaders.valueAt(i); 206 } else { 207 mBaseURL = mSessionURL; 208 } 209 } 210 211 CHECK_GT(mSessionDesc->countTracks(), 1u); 212 setupTrack(1); 213 } else { 214 sp<AMessage> reply = new AMessage('disc', id()); 215 mConn->disconnect(reply); 216 } 217 break; 218 } 219 220 case 'setu': 221 { 222 size_t index; 223 CHECK(msg->findSize("index", &index)); 224 225 TrackInfo *track = NULL; 226 size_t trackIndex; 227 if (msg->findSize("track-index", &trackIndex)) { 228 track = &mTracks.editItemAt(trackIndex); 229 } 230 231 int32_t result; 232 CHECK(msg->findInt32("result", &result)); 233 234 LOG(INFO) << "SETUP(" << index << ") completed with result " 235 << result << " (" << strerror(-result) << ")"; 236 237 if (result != OK) { 238 if (track) { 239 if (!track->mUsingInterleavedTCP) { 240 close(track->mRTPSocket); 241 close(track->mRTCPSocket); 242 } 243 244 mTracks.removeItemsAt(trackIndex); 245 } 246 } else { 247 CHECK(track != NULL); 248 249 sp<RefBase> obj; 250 CHECK(msg->findObject("response", &obj)); 251 sp<ARTSPResponse> response = 252 static_cast<ARTSPResponse *>(obj.get()); 253 254 CHECK_EQ(response->mStatusCode, 200u); 255 256 ssize_t i = response->mHeaders.indexOfKey("session"); 257 CHECK_GE(i, 0); 258 259 if (index == 1) { 260 mSessionID = response->mHeaders.valueAt(i); 261 i = mSessionID.find(";"); 262 if (i >= 0) { 263 // Remove options, i.e. ";timeout=90" 264 mSessionID.erase(i, mSessionID.size() - i); 265 } 266 } 267 268 sp<AMessage> notify = new AMessage('accu', id()); 269 notify->setSize("track-index", trackIndex); 270 271 mRTPConn->addStream( 272 track->mRTPSocket, track->mRTCPSocket, 273 mSessionDesc, index, 274 notify, track->mUsingInterleavedTCP); 275 276 mSetupTracksSuccessful = true; 277 } 278 279 ++index; 280 if (index < mSessionDesc->countTracks()) { 281 setupTrack(index); 282 } else if (mSetupTracksSuccessful) { 283 AString request = "PLAY "; 284 request.append(mSessionURL); 285 request.append(" RTSP/1.0\r\n"); 286 287 request.append("Session: "); 288 request.append(mSessionID); 289 request.append("\r\n"); 290 291 request.append("\r\n"); 292 293 sp<AMessage> reply = new AMessage('play', id()); 294 mConn->sendRequest(request.c_str(), reply); 295 } else { 296 sp<AMessage> reply = new AMessage('disc', id()); 297 mConn->disconnect(reply); 298 } 299 break; 300 } 301 302 case 'play': 303 { 304 int32_t result; 305 CHECK(msg->findInt32("result", &result)); 306 307 LOG(INFO) << "PLAY completed with result " 308 << result << " (" << strerror(-result) << ")"; 309 310 if (result == OK) { 311 sp<RefBase> obj; 312 CHECK(msg->findObject("response", &obj)); 313 sp<ARTSPResponse> response = 314 static_cast<ARTSPResponse *>(obj.get()); 315 316 CHECK_EQ(response->mStatusCode, 200u); 317 318 parsePlayResponse(response); 319 320 mDoneMsg->setInt32("result", OK); 321 mDoneMsg->post(); 322 mDoneMsg = NULL; 323 324 sp<AMessage> timeout = new AMessage('tiou', id()); 325 timeout->post(10000000ll); 326 } else { 327 sp<AMessage> reply = new AMessage('disc', id()); 328 mConn->disconnect(reply); 329 } 330 331 break; 332 } 333 334 case 'abor': 335 { 336 for (size_t i = 0; i < mTracks.size(); ++i) { 337 mTracks.editItemAt(i).mPacketSource->signalEOS( 338 ERROR_END_OF_STREAM); 339 } 340 341 sp<AMessage> reply = new AMessage('tear', id()); 342 343 AString request; 344 request = "TEARDOWN "; 345 346 // XXX should use aggregate url from SDP here... 347 request.append(mSessionURL); 348 request.append(" RTSP/1.0\r\n"); 349 350 request.append("Session: "); 351 request.append(mSessionID); 352 request.append("\r\n"); 353 354 request.append("\r\n"); 355 356 mConn->sendRequest(request.c_str(), reply); 357 break; 358 } 359 360 case 'tear': 361 { 362 int32_t result; 363 CHECK(msg->findInt32("result", &result)); 364 365 LOG(INFO) << "TEARDOWN completed with result " 366 << result << " (" << strerror(-result) << ")"; 367 368 sp<AMessage> reply = new AMessage('disc', id()); 369 mConn->disconnect(reply); 370 break; 371 } 372 373 case 'quit': 374 { 375 if (mDoneMsg != NULL) { 376 mDoneMsg->setInt32("result", UNKNOWN_ERROR); 377 mDoneMsg->post(); 378 mDoneMsg = NULL; 379 } 380 break; 381 } 382 383 case 'chek': 384 { 385 if (mNumAccessUnitsReceived == 0) { 386 LOG(INFO) << "stream ended? aborting."; 387 (new AMessage('abor', id()))->post(); 388 break; 389 } 390 391 mNumAccessUnitsReceived = 0; 392 msg->post(500000); 393 break; 394 } 395 396 case 'accu': 397 { 398 ++mNumAccessUnitsReceived; 399 400 if (!mCheckPending) { 401 mCheckPending = true; 402 sp<AMessage> check = new AMessage('chek', id()); 403 check->post(500000); 404 } 405 406 size_t trackIndex; 407 CHECK(msg->findSize("track-index", &trackIndex)); 408 409 TrackInfo *track = &mTracks.editItemAt(trackIndex); 410 411 int32_t eos; 412 if (msg->findInt32("eos", &eos)) { 413 LOG(INFO) << "received BYE on track index " << trackIndex; 414#if 0 415 track->mPacketSource->signalEOS(ERROR_END_OF_STREAM); 416#endif 417 return; 418 } 419 420 sp<RefBase> obj; 421 CHECK(msg->findObject("access-unit", &obj)); 422 423 sp<ABuffer> accessUnit = static_cast<ABuffer *>(obj.get()); 424 425 uint32_t seqNum = (uint32_t)accessUnit->int32Data(); 426 427 if (seqNum < track->mFirstSeqNumInSegment) { 428 LOG(INFO) << "dropping stale access-unit " 429 << "(" << seqNum << " < " 430 << track->mFirstSeqNumInSegment << ")"; 431 break; 432 } 433 434 uint64_t ntpTime; 435 CHECK(accessUnit->meta()->findInt64( 436 "ntp-time", (int64_t *)&ntpTime)); 437 438 uint32_t rtpTime; 439 CHECK(accessUnit->meta()->findInt32( 440 "rtp-time", (int32_t *)&rtpTime)); 441 442 if (track->mNewSegment) { 443 track->mNewSegment = false; 444 445 LOG(VERBOSE) << "first segment unit ntpTime=" 446 << StringPrintf("0x%016llx", ntpTime) 447 << " rtpTime=" << rtpTime 448 << " seq=" << seqNum; 449 } 450 451 if (mFirstAccessUnit) { 452 mFirstAccessUnit = false; 453 mFirstAccessUnitNTP = ntpTime; 454 } 455 456 if (ntpTime >= mFirstAccessUnitNTP) { 457 ntpTime -= mFirstAccessUnitNTP; 458 } else { 459 ntpTime = 0; 460 } 461 462 int64_t timeUs = (int64_t)(ntpTime * 1E6 / (1ll << 32)); 463 464 accessUnit->meta()->setInt64("timeUs", timeUs); 465 466#if 0 467 int32_t damaged; 468 if (accessUnit->meta()->findInt32("damaged", &damaged) 469 && damaged != 0) { 470 LOG(INFO) << "ignoring damaged AU"; 471 } else 472#endif 473 { 474 TrackInfo *track = &mTracks.editItemAt(trackIndex); 475 track->mPacketSource->queueAccessUnit(accessUnit); 476 } 477 break; 478 } 479 480 case 'seek': 481 { 482 if (mSeekPending) { 483 break; 484 } 485 486 int64_t timeUs; 487 CHECK(msg->findInt64("time", &timeUs)); 488 489 mSeekPending = true; 490 491 AString request = "PAUSE "; 492 request.append(mSessionURL); 493 request.append(" RTSP/1.0\r\n"); 494 495 request.append("Session: "); 496 request.append(mSessionID); 497 request.append("\r\n"); 498 499 request.append("\r\n"); 500 501 sp<AMessage> reply = new AMessage('see1', id()); 502 reply->setInt64("time", timeUs); 503 mConn->sendRequest(request.c_str(), reply); 504 break; 505 } 506 507 case 'see1': 508 { 509 // Session is paused now. 510 for (size_t i = 0; i < mTracks.size(); ++i) { 511 mTracks.editItemAt(i).mPacketSource->flushQueue(); 512 } 513 514 int64_t timeUs; 515 CHECK(msg->findInt64("time", &timeUs)); 516 517 AString request = "PLAY "; 518 request.append(mSessionURL); 519 request.append(" RTSP/1.0\r\n"); 520 521 request.append("Session: "); 522 request.append(mSessionID); 523 request.append("\r\n"); 524 525 request.append( 526 StringPrintf( 527 "Range: npt=%lld-\r\n", timeUs / 1000000ll)); 528 529 request.append("\r\n"); 530 531 sp<AMessage> reply = new AMessage('see2', id()); 532 mConn->sendRequest(request.c_str(), reply); 533 break; 534 } 535 536 case 'see2': 537 { 538 CHECK(mSeekPending); 539 540 int32_t result; 541 CHECK(msg->findInt32("result", &result)); 542 543 LOG(INFO) << "PLAY completed with result " 544 << result << " (" << strerror(-result) << ")"; 545 546 CHECK_EQ(result, (status_t)OK); 547 548 sp<RefBase> obj; 549 CHECK(msg->findObject("response", &obj)); 550 sp<ARTSPResponse> response = 551 static_cast<ARTSPResponse *>(obj.get()); 552 553 CHECK_EQ(response->mStatusCode, 200u); 554 555 parsePlayResponse(response); 556 557 LOG(INFO) << "seek completed."; 558 mSeekPending = false; 559 break; 560 } 561 562 case 'biny': 563 { 564 sp<RefBase> obj; 565 CHECK(msg->findObject("buffer", &obj)); 566 sp<ABuffer> buffer = static_cast<ABuffer *>(obj.get()); 567 568 int32_t index; 569 CHECK(buffer->meta()->findInt32("index", &index)); 570 571 mRTPConn->injectPacket(index, buffer); 572 break; 573 } 574 575 case 'tiou': 576 { 577 if (mFirstAccessUnit) { 578 LOG(WARNING) << "Never received any data, disconnecting."; 579 (new AMessage('abor', id()))->post(); 580 } 581 break; 582 } 583 584 default: 585 TRESPASS(); 586 break; 587 } 588 } 589 590 static void SplitString( 591 const AString &s, const char *separator, List<AString> *items) { 592 items->clear(); 593 size_t start = 0; 594 while (start < s.size()) { 595 ssize_t offset = s.find(separator, start); 596 597 if (offset < 0) { 598 items->push_back(AString(s, start, s.size() - start)); 599 break; 600 } 601 602 items->push_back(AString(s, start, offset - start)); 603 start = offset + strlen(separator); 604 } 605 } 606 607 void parsePlayResponse(const sp<ARTSPResponse> &response) { 608 ssize_t i = response->mHeaders.indexOfKey("range"); 609 if (i < 0) { 610 // Server doesn't even tell use what range it is going to 611 // play, therefore we won't support seeking. 612 return; 613 } 614 615 AString range = response->mHeaders.valueAt(i); 616 LOG(VERBOSE) << "Range: " << range; 617 618 AString val; 619 CHECK(GetAttribute(range.c_str(), "npt", &val)); 620 float npt1, npt2; 621 622 if (val == "now-") { 623 // This is a live stream and therefore not seekable. 624 return; 625 } else { 626 CHECK_EQ(sscanf(val.c_str(), "%f-%f", &npt1, &npt2), 2); 627 } 628 629 i = response->mHeaders.indexOfKey("rtp-info"); 630 CHECK_GE(i, 0); 631 632 AString rtpInfo = response->mHeaders.valueAt(i); 633 List<AString> streamInfos; 634 SplitString(rtpInfo, ",", &streamInfos); 635 636 int n = 1; 637 for (List<AString>::iterator it = streamInfos.begin(); 638 it != streamInfos.end(); ++it) { 639 (*it).trim(); 640 LOG(VERBOSE) << "streamInfo[" << n << "] = " << *it; 641 642 CHECK(GetAttribute((*it).c_str(), "url", &val)); 643 644 size_t trackIndex = 0; 645 while (trackIndex < mTracks.size() 646 && !(val == mTracks.editItemAt(trackIndex).mURL)) { 647 ++trackIndex; 648 } 649 CHECK_LT(trackIndex, mTracks.size()); 650 651 CHECK(GetAttribute((*it).c_str(), "seq", &val)); 652 653 char *end; 654 unsigned long seq = strtoul(val.c_str(), &end, 10); 655 656 TrackInfo *info = &mTracks.editItemAt(trackIndex); 657 info->mFirstSeqNumInSegment = seq; 658 info->mNewSegment = true; 659 660 CHECK(GetAttribute((*it).c_str(), "rtptime", &val)); 661 662 uint32_t rtpTime = strtoul(val.c_str(), &end, 10); 663 664 LOG(VERBOSE) << "track #" << n 665 << ": rtpTime=" << rtpTime << " <=> npt=" << npt1; 666 667 info->mPacketSource->setNormalPlayTimeMapping( 668 rtpTime, (int64_t)(npt1 * 1E6)); 669 670 ++n; 671 } 672 } 673 674 sp<APacketSource> getPacketSource(size_t index) { 675 CHECK_GE(index, 0u); 676 CHECK_LT(index, mTracks.size()); 677 678 return mTracks.editItemAt(index).mPacketSource; 679 } 680 681 size_t countTracks() const { 682 return mTracks.size(); 683 } 684 685private: 686 sp<ALooper> mLooper; 687 sp<ALooper> mNetLooper; 688 sp<ARTSPConnection> mConn; 689 sp<ARTPConnection> mRTPConn; 690 sp<ASessionDescription> mSessionDesc; 691 AString mSessionURL; 692 AString mBaseURL; 693 AString mSessionID; 694 bool mSetupTracksSuccessful; 695 bool mSeekPending; 696 bool mFirstAccessUnit; 697 uint64_t mFirstAccessUnitNTP; 698 int64_t mNumAccessUnitsReceived; 699 bool mCheckPending; 700 701 struct TrackInfo { 702 AString mURL; 703 int mRTPSocket; 704 int mRTCPSocket; 705 bool mUsingInterleavedTCP; 706 uint32_t mFirstSeqNumInSegment; 707 bool mNewSegment; 708 709 sp<APacketSource> mPacketSource; 710 }; 711 Vector<TrackInfo> mTracks; 712 713 sp<AMessage> mDoneMsg; 714 715 void setupTrack(size_t index) { 716 sp<APacketSource> source = 717 new APacketSource(mSessionDesc, index); 718 if (source->initCheck() != OK) { 719 LOG(WARNING) << "Unsupported format. Ignoring track #" 720 << index << "."; 721 722 sp<AMessage> reply = new AMessage('setu', id()); 723 reply->setSize("index", index); 724 reply->setInt32("result", ERROR_UNSUPPORTED); 725 reply->post(); 726 return; 727 } 728 729 AString url; 730 CHECK(mSessionDesc->findAttribute(index, "a=control", &url)); 731 732 AString trackURL; 733 CHECK(MakeURL(mBaseURL.c_str(), url.c_str(), &trackURL)); 734 735 mTracks.push(TrackInfo()); 736 TrackInfo *info = &mTracks.editItemAt(mTracks.size() - 1); 737 info->mURL = trackURL; 738 info->mPacketSource = source; 739 info->mUsingInterleavedTCP = false; 740 info->mFirstSeqNumInSegment = 0; 741 info->mNewSegment = true; 742 743 LOG(VERBOSE) << "track #" << mTracks.size() << " URL=" << trackURL; 744 745 AString request = "SETUP "; 746 request.append(trackURL); 747 request.append(" RTSP/1.0\r\n"); 748 749#if USE_TCP_INTERLEAVED 750 size_t interleaveIndex = 2 * (mTracks.size() - 1); 751 info->mUsingInterleavedTCP = true; 752 info->mRTPSocket = interleaveIndex; 753 info->mRTCPSocket = interleaveIndex + 1; 754 755 request.append("Transport: RTP/AVP/TCP;interleaved="); 756 request.append(interleaveIndex); 757 request.append("-"); 758 request.append(interleaveIndex + 1); 759#else 760 unsigned rtpPort; 761 ARTPConnection::MakePortPair( 762 &info->mRTPSocket, &info->mRTCPSocket, &rtpPort); 763 764 request.append("Transport: RTP/AVP/UDP;unicast;client_port="); 765 request.append(rtpPort); 766 request.append("-"); 767 request.append(rtpPort + 1); 768#endif 769 770 request.append("\r\n"); 771 772 if (index > 1) { 773 request.append("Session: "); 774 request.append(mSessionID); 775 request.append("\r\n"); 776 } 777 778 request.append("\r\n"); 779 780 sp<AMessage> reply = new AMessage('setu', id()); 781 reply->setSize("index", index); 782 reply->setSize("track-index", mTracks.size() - 1); 783 mConn->sendRequest(request.c_str(), reply); 784 } 785 786 static bool MakeURL(const char *baseURL, const char *url, AString *out) { 787 out->clear(); 788 789 if (strncasecmp("rtsp://", baseURL, 7)) { 790 // Base URL must be absolute 791 return false; 792 } 793 794 if (!strncasecmp("rtsp://", url, 7)) { 795 // "url" is already an absolute URL, ignore base URL. 796 out->setTo(url); 797 return true; 798 } 799 800 size_t n = strlen(baseURL); 801 if (baseURL[n - 1] == '/') { 802 out->setTo(baseURL); 803 out->append(url); 804 } else { 805 const char *slashPos = strrchr(baseURL, '/'); 806 807 if (slashPos > &baseURL[6]) { 808 out->setTo(baseURL, slashPos - baseURL); 809 } else { 810 out->setTo(baseURL); 811 } 812 813 out->append("/"); 814 out->append(url); 815 } 816 817 return true; 818 } 819 820 DISALLOW_EVIL_CONSTRUCTORS(MyHandler); 821}; 822 823} // namespace android 824 825#endif // MY_HANDLER_H_ 826