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