LiveSession.cpp revision 0f30bd90272c818aa37c0bb22d22eaa7d3689879
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//#define LOG_NDEBUG 0
18#define LOG_TAG "LiveSession"
19#include <utils/Log.h>
20
21#include "include/LiveSession.h"
22
23#include "LiveDataSource.h"
24
25#include "include/M3UParser.h"
26#include "include/HTTPBase.h"
27
28#include <cutils/properties.h>
29#include <media/stagefright/foundation/hexdump.h>
30#include <media/stagefright/foundation/ABuffer.h>
31#include <media/stagefright/foundation/ADebug.h>
32#include <media/stagefright/foundation/AMessage.h>
33#include <media/stagefright/DataSource.h>
34#include <media/stagefright/FileSource.h>
35#include <media/stagefright/MediaErrors.h>
36
37#include <ctype.h>
38#include <openssl/aes.h>
39
40namespace android {
41
42const int64_t LiveSession::kMaxPlaylistAgeUs = 15000000ll;
43
44LiveSession::LiveSession(uint32_t flags)
45    : mFlags(flags),
46      mDataSource(new LiveDataSource),
47      mHTTPDataSource(
48              HTTPBase::Create(
49                  (mFlags & kFlagIncognito)
50                    ? HTTPBase::kFlagIncognito
51                    : 0)),
52      mPrevBandwidthIndex(-1),
53      mLastPlaylistFetchTimeUs(-1),
54      mSeqNumber(-1),
55      mSeekTimeUs(-1),
56      mNumRetries(0),
57      mDurationUs(-1),
58      mSeekDone(false),
59      mDisconnectPending(false),
60      mMonitorQueueGeneration(0) {
61}
62
63LiveSession::~LiveSession() {
64}
65
66sp<DataSource> LiveSession::getDataSource() {
67    return mDataSource;
68}
69
70void LiveSession::connect(
71        const char *url, const KeyedVector<String8, String8> *headers) {
72    sp<AMessage> msg = new AMessage(kWhatConnect, id());
73    msg->setString("url", url);
74
75    if (headers != NULL) {
76        msg->setPointer(
77                "headers",
78                new KeyedVector<String8, String8>(*headers));
79    }
80
81    msg->post();
82}
83
84void LiveSession::disconnect() {
85    Mutex::Autolock autoLock(mLock);
86    mDisconnectPending = true;
87
88    mHTTPDataSource->disconnect();
89
90    (new AMessage(kWhatDisconnect, id()))->post();
91}
92
93void LiveSession::seekTo(int64_t timeUs) {
94    Mutex::Autolock autoLock(mLock);
95    mSeekDone = false;
96
97    sp<AMessage> msg = new AMessage(kWhatSeek, id());
98    msg->setInt64("timeUs", timeUs);
99    msg->post();
100
101    while (!mSeekDone) {
102        mCondition.wait(mLock);
103    }
104}
105
106void LiveSession::onMessageReceived(const sp<AMessage> &msg) {
107    switch (msg->what()) {
108        case kWhatConnect:
109            onConnect(msg);
110            break;
111
112        case kWhatDisconnect:
113            onDisconnect();
114            break;
115
116        case kWhatMonitorQueue:
117        {
118            int32_t generation;
119            CHECK(msg->findInt32("generation", &generation));
120
121            if (generation != mMonitorQueueGeneration) {
122                // Stale event
123                break;
124            }
125
126            onMonitorQueue();
127            break;
128        }
129
130        case kWhatSeek:
131            onSeek(msg);
132            break;
133
134        default:
135            TRESPASS();
136            break;
137    }
138}
139
140// static
141int LiveSession::SortByBandwidth(const BandwidthItem *a, const BandwidthItem *b) {
142    if (a->mBandwidth < b->mBandwidth) {
143        return -1;
144    } else if (a->mBandwidth == b->mBandwidth) {
145        return 0;
146    }
147
148    return 1;
149}
150
151void LiveSession::onConnect(const sp<AMessage> &msg) {
152    AString url;
153    CHECK(msg->findString("url", &url));
154
155    KeyedVector<String8, String8> *headers = NULL;
156    if (!msg->findPointer("headers", (void **)&headers)) {
157        mExtraHeaders.clear();
158    } else {
159        mExtraHeaders = *headers;
160
161        delete headers;
162        headers = NULL;
163    }
164
165    if (!(mFlags & kFlagIncognito)) {
166        LOGI("onConnect '%s'", url.c_str());
167    } else {
168        LOGI("onConnect <URL suppressed>");
169    }
170
171    mMasterURL = url;
172
173    sp<M3UParser> playlist = fetchPlaylist(url.c_str());
174
175    if (playlist == NULL) {
176        LOGE("unable to fetch master playlist '%s'.", url.c_str());
177
178        mDataSource->queueEOS(ERROR_IO);
179        return;
180    }
181
182    if (playlist->isVariantPlaylist()) {
183        for (size_t i = 0; i < playlist->size(); ++i) {
184            BandwidthItem item;
185
186            sp<AMessage> meta;
187            playlist->itemAt(i, &item.mURI, &meta);
188
189            unsigned long bandwidth;
190            CHECK(meta->findInt32("bandwidth", (int32_t *)&item.mBandwidth));
191
192            mBandwidthItems.push(item);
193        }
194
195        CHECK_GT(mBandwidthItems.size(), 0u);
196
197        mBandwidthItems.sort(SortByBandwidth);
198    }
199
200    postMonitorQueue();
201}
202
203void LiveSession::onDisconnect() {
204    LOGI("onDisconnect");
205
206    mDataSource->queueEOS(ERROR_END_OF_STREAM);
207
208    Mutex::Autolock autoLock(mLock);
209    mDisconnectPending = false;
210}
211
212status_t LiveSession::fetchFile(const char *url, sp<ABuffer> *out) {
213    *out = NULL;
214
215    sp<DataSource> source;
216
217    if (!strncasecmp(url, "file://", 7)) {
218        source = new FileSource(url + 7);
219    } else if (strncasecmp(url, "http://", 7)
220            && strncasecmp(url, "https://", 8)) {
221        return ERROR_UNSUPPORTED;
222    } else {
223        {
224            Mutex::Autolock autoLock(mLock);
225
226            if (mDisconnectPending) {
227                return ERROR_IO;
228            }
229        }
230
231        status_t err = mHTTPDataSource->connect(
232                url, mExtraHeaders.isEmpty() ? NULL : &mExtraHeaders);
233
234        if (err != OK) {
235            return err;
236        }
237
238        source = mHTTPDataSource;
239    }
240
241    off64_t size;
242    status_t err = source->getSize(&size);
243
244    if (err != OK) {
245        size = 65536;
246    }
247
248    sp<ABuffer> buffer = new ABuffer(size);
249    buffer->setRange(0, 0);
250
251    for (;;) {
252        size_t bufferRemaining = buffer->capacity() - buffer->size();
253
254        if (bufferRemaining == 0) {
255            bufferRemaining = 32768;
256
257            LOGV("increasing download buffer to %d bytes",
258                 buffer->size() + bufferRemaining);
259
260            sp<ABuffer> copy = new ABuffer(buffer->size() + bufferRemaining);
261            memcpy(copy->data(), buffer->data(), buffer->size());
262            copy->setRange(0, buffer->size());
263
264            buffer = copy;
265        }
266
267        ssize_t n = source->readAt(
268                buffer->size(), buffer->data() + buffer->size(),
269                bufferRemaining);
270
271        if (n < 0) {
272            return n;
273        }
274
275        if (n == 0) {
276            break;
277        }
278
279        buffer->setRange(0, buffer->size() + (size_t)n);
280    }
281
282    *out = buffer;
283
284    return OK;
285}
286
287sp<M3UParser> LiveSession::fetchPlaylist(const char *url) {
288    sp<ABuffer> buffer;
289    status_t err = fetchFile(url, &buffer);
290
291    if (err != OK) {
292        return NULL;
293    }
294
295    sp<M3UParser> playlist =
296        new M3UParser(url, buffer->data(), buffer->size());
297
298    if (playlist->initCheck() != OK) {
299        LOGE("failed to parse .m3u8 playlist");
300
301        return NULL;
302    }
303
304    return playlist;
305}
306
307static double uniformRand() {
308    return (double)rand() / RAND_MAX;
309}
310
311size_t LiveSession::getBandwidthIndex() {
312    if (mBandwidthItems.size() == 0) {
313        return 0;
314    }
315
316#if 1
317    int32_t bandwidthBps;
318    if (mHTTPDataSource != NULL
319            && mHTTPDataSource->estimateBandwidth(&bandwidthBps)) {
320        LOGV("bandwidth estimated at %.2f kbps", bandwidthBps / 1024.0f);
321    } else {
322        LOGV("no bandwidth estimate.");
323        return 0;  // Pick the lowest bandwidth stream by default.
324    }
325
326    char value[PROPERTY_VALUE_MAX];
327    if (property_get("media.httplive.max-bw", value, NULL)) {
328        char *end;
329        long maxBw = strtoul(value, &end, 10);
330        if (end > value && *end == '\0') {
331            if (maxBw > 0 && bandwidthBps > maxBw) {
332                LOGV("bandwidth capped to %ld bps", maxBw);
333                bandwidthBps = maxBw;
334            }
335        }
336    }
337
338    // Consider only 80% of the available bandwidth usable.
339    bandwidthBps = (bandwidthBps * 8) / 10;
340
341    // Pick the highest bandwidth stream below or equal to estimated bandwidth.
342
343    size_t index = mBandwidthItems.size() - 1;
344    while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth
345                            > (size_t)bandwidthBps) {
346        --index;
347    }
348#elif 0
349    // Change bandwidth at random()
350    size_t index = uniformRand() * mBandwidthItems.size();
351#elif 0
352    // There's a 50% chance to stay on the current bandwidth and
353    // a 50% chance to switch to the next higher bandwidth (wrapping around
354    // to lowest)
355    const size_t kMinIndex = 0;
356
357    size_t index;
358    if (mPrevBandwidthIndex < 0) {
359        index = kMinIndex;
360    } else if (uniformRand() < 0.5) {
361        index = (size_t)mPrevBandwidthIndex;
362    } else {
363        index = mPrevBandwidthIndex + 1;
364        if (index == mBandwidthItems.size()) {
365            index = kMinIndex;
366        }
367    }
368#elif 0
369    // Pick the highest bandwidth stream below or equal to 1.2 Mbit/sec
370
371    size_t index = mBandwidthItems.size() - 1;
372    while (index > 0 && mBandwidthItems.itemAt(index).mBandwidth > 1200000) {
373        --index;
374    }
375#else
376    size_t index = mBandwidthItems.size() - 1;  // Highest bandwidth stream
377#endif
378
379    return index;
380}
381
382void LiveSession::onDownloadNext() {
383    size_t bandwidthIndex = getBandwidthIndex();
384
385rinse_repeat:
386    int64_t nowUs = ALooper::GetNowUs();
387
388    if (mLastPlaylistFetchTimeUs < 0
389            || (ssize_t)bandwidthIndex != mPrevBandwidthIndex
390            || (!mPlaylist->isComplete()
391                && mLastPlaylistFetchTimeUs + kMaxPlaylistAgeUs <= nowUs)) {
392        AString url;
393        if (mBandwidthItems.size() > 0) {
394            url = mBandwidthItems.editItemAt(bandwidthIndex).mURI;
395        } else {
396            url = mMasterURL;
397        }
398
399        bool firstTime = (mPlaylist == NULL);
400
401        mPlaylist = fetchPlaylist(url.c_str());
402        if (mPlaylist == NULL) {
403            LOGE("failed to load playlist at url '%s'", url.c_str());
404            mDataSource->queueEOS(ERROR_IO);
405            return;
406        }
407
408        if (firstTime) {
409            Mutex::Autolock autoLock(mLock);
410
411            if (!mPlaylist->isComplete()) {
412                mDurationUs = -1;
413            } else {
414                mDurationUs = 0;
415                for (size_t i = 0; i < mPlaylist->size(); ++i) {
416                    sp<AMessage> itemMeta;
417                    CHECK(mPlaylist->itemAt(
418                                i, NULL /* uri */, &itemMeta));
419
420                    int64_t itemDurationUs;
421                    CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
422
423                    mDurationUs += itemDurationUs;
424                }
425            }
426        }
427
428        mLastPlaylistFetchTimeUs = ALooper::GetNowUs();
429    }
430
431    int32_t firstSeqNumberInPlaylist;
432    if (mPlaylist->meta() == NULL || !mPlaylist->meta()->findInt32(
433                "media-sequence", &firstSeqNumberInPlaylist)) {
434        firstSeqNumberInPlaylist = 0;
435    }
436
437    bool explicitDiscontinuity = false;
438    bool bandwidthChanged = false;
439
440    if (mSeekTimeUs >= 0) {
441        if (mPlaylist->isComplete()) {
442            size_t index = 0;
443            int64_t segmentStartUs = 0;
444            while (index < mPlaylist->size()) {
445                sp<AMessage> itemMeta;
446                CHECK(mPlaylist->itemAt(
447                            index, NULL /* uri */, &itemMeta));
448
449                int64_t itemDurationUs;
450                CHECK(itemMeta->findInt64("durationUs", &itemDurationUs));
451
452                if (mSeekTimeUs < segmentStartUs + itemDurationUs) {
453                    break;
454                }
455
456                segmentStartUs += itemDurationUs;
457                ++index;
458            }
459
460            if (index < mPlaylist->size()) {
461                int32_t newSeqNumber = firstSeqNumberInPlaylist + index;
462
463                if (newSeqNumber != mSeqNumber) {
464                    LOGI("seeking to seq no %d", newSeqNumber);
465
466                    mSeqNumber = newSeqNumber;
467
468                    mDataSource->reset();
469
470                    // reseting the data source will have had the
471                    // side effect of discarding any previously queued
472                    // bandwidth change discontinuity.
473                    // Therefore we'll need to treat these explicit
474                    // discontinuities as involving a bandwidth change
475                    // even if they aren't directly.
476                    explicitDiscontinuity = true;
477                    bandwidthChanged = true;
478                }
479            }
480        }
481
482        mSeekTimeUs = -1;
483
484        Mutex::Autolock autoLock(mLock);
485        mSeekDone = true;
486        mCondition.broadcast();
487    }
488
489    if (mSeqNumber < 0) {
490        if (mPlaylist->isComplete()) {
491            mSeqNumber = firstSeqNumberInPlaylist;
492        } else {
493            mSeqNumber = firstSeqNumberInPlaylist + mPlaylist->size() / 2;
494        }
495    }
496
497    int32_t lastSeqNumberInPlaylist =
498        firstSeqNumberInPlaylist + (int32_t)mPlaylist->size() - 1;
499
500    if (mSeqNumber < firstSeqNumberInPlaylist
501            || mSeqNumber > lastSeqNumberInPlaylist) {
502        if (mPrevBandwidthIndex != (ssize_t)bandwidthIndex) {
503            // Go back to the previous bandwidth.
504
505            LOGI("new bandwidth does not have the sequence number "
506                 "we're looking for, switching back to previous bandwidth");
507
508            mLastPlaylistFetchTimeUs = -1;
509            bandwidthIndex = mPrevBandwidthIndex;
510            goto rinse_repeat;
511        }
512
513        if (!mPlaylist->isComplete()
514                && mSeqNumber > lastSeqNumberInPlaylist
515                && mNumRetries < kMaxNumRetries) {
516            ++mNumRetries;
517
518            mLastPlaylistFetchTimeUs = -1;
519            postMonitorQueue(3000000ll);
520            return;
521        }
522
523        LOGE("Cannot find sequence number %d in playlist "
524             "(contains %d - %d)",
525             mSeqNumber, firstSeqNumberInPlaylist,
526             firstSeqNumberInPlaylist + mPlaylist->size() - 1);
527
528        mDataSource->queueEOS(ERROR_END_OF_STREAM);
529        return;
530    }
531
532    mNumRetries = 0;
533
534    AString uri;
535    sp<AMessage> itemMeta;
536    CHECK(mPlaylist->itemAt(
537                mSeqNumber - firstSeqNumberInPlaylist,
538                &uri,
539                &itemMeta));
540
541    int32_t val;
542    if (itemMeta->findInt32("discontinuity", &val) && val != 0) {
543        explicitDiscontinuity = true;
544    }
545
546    sp<ABuffer> buffer;
547    status_t err = fetchFile(uri.c_str(), &buffer);
548    if (err != OK) {
549        LOGE("failed to fetch .ts segment at url '%s'", uri.c_str());
550        mDataSource->queueEOS(err);
551        return;
552    }
553
554    CHECK(buffer != NULL);
555
556    err = decryptBuffer(mSeqNumber - firstSeqNumberInPlaylist, buffer);
557
558    if (err != OK) {
559        LOGE("decryptBuffer failed w/ error %d", err);
560
561        mDataSource->queueEOS(err);
562        return;
563    }
564
565    if (buffer->size() == 0 || buffer->data()[0] != 0x47) {
566        // Not a transport stream???
567
568        LOGE("This doesn't look like a transport stream...");
569
570        mBandwidthItems.removeAt(bandwidthIndex);
571
572        if (mBandwidthItems.isEmpty()) {
573            mDataSource->queueEOS(ERROR_UNSUPPORTED);
574            return;
575        }
576
577        LOGI("Retrying with a different bandwidth stream.");
578
579        mLastPlaylistFetchTimeUs = -1;
580        bandwidthIndex = getBandwidthIndex();
581        mPrevBandwidthIndex = bandwidthIndex;
582        mSeqNumber = -1;
583
584        goto rinse_repeat;
585    }
586
587    if ((size_t)mPrevBandwidthIndex != bandwidthIndex) {
588        bandwidthChanged = true;
589    }
590
591    if (mPrevBandwidthIndex < 0) {
592        // Don't signal a bandwidth change at the very beginning of
593        // playback.
594        bandwidthChanged = false;
595    }
596
597    if (explicitDiscontinuity || bandwidthChanged) {
598        // Signal discontinuity.
599
600        LOGI("queueing discontinuity (explicit=%d, bandwidthChanged=%d)",
601             explicitDiscontinuity, bandwidthChanged);
602
603        sp<ABuffer> tmp = new ABuffer(188);
604        memset(tmp->data(), 0, tmp->size());
605        tmp->data()[1] = bandwidthChanged;
606
607        mDataSource->queueBuffer(tmp);
608    }
609
610    mDataSource->queueBuffer(buffer);
611
612    mPrevBandwidthIndex = bandwidthIndex;
613    ++mSeqNumber;
614
615    postMonitorQueue();
616}
617
618void LiveSession::onMonitorQueue() {
619    if (mSeekTimeUs >= 0
620            || mDataSource->countQueuedBuffers() < kMaxNumQueuedFragments) {
621        onDownloadNext();
622    } else {
623        postMonitorQueue(1000000ll);
624    }
625}
626
627status_t LiveSession::decryptBuffer(
628        size_t playlistIndex, const sp<ABuffer> &buffer) {
629    sp<AMessage> itemMeta;
630    bool found = false;
631    AString method;
632
633    for (ssize_t i = playlistIndex; i >= 0; --i) {
634        AString uri;
635        CHECK(mPlaylist->itemAt(i, &uri, &itemMeta));
636
637        if (itemMeta->findString("cipher-method", &method)) {
638            found = true;
639            break;
640        }
641    }
642
643    if (!found) {
644        method = "NONE";
645    }
646
647    if (method == "NONE") {
648        return OK;
649    } else if (!(method == "AES-128")) {
650        LOGE("Unsupported cipher method '%s'", method.c_str());
651        return ERROR_UNSUPPORTED;
652    }
653
654    AString keyURI;
655    if (!itemMeta->findString("cipher-uri", &keyURI)) {
656        LOGE("Missing key uri");
657        return ERROR_MALFORMED;
658    }
659
660    ssize_t index = mAESKeyForURI.indexOfKey(keyURI);
661
662    sp<ABuffer> key;
663    if (index >= 0) {
664        key = mAESKeyForURI.valueAt(index);
665    } else {
666        key = new ABuffer(16);
667
668        sp<HTTPBase> keySource =
669              HTTPBase::Create(
670                  (mFlags & kFlagIncognito)
671                    ? HTTPBase::kFlagIncognito
672                    : 0);
673
674        status_t err = keySource->connect(keyURI.c_str());
675
676        if (err == OK) {
677            size_t offset = 0;
678            while (offset < 16) {
679                ssize_t n = keySource->readAt(
680                        offset, key->data() + offset, 16 - offset);
681                if (n <= 0) {
682                    err = ERROR_IO;
683                    break;
684                }
685
686                offset += n;
687            }
688        }
689
690        if (err != OK) {
691            LOGE("failed to fetch cipher key from '%s'.", keyURI.c_str());
692            return ERROR_IO;
693        }
694
695        mAESKeyForURI.add(keyURI, key);
696    }
697
698    AES_KEY aes_key;
699    if (AES_set_decrypt_key(key->data(), 128, &aes_key) != 0) {
700        LOGE("failed to set AES decryption key.");
701        return UNKNOWN_ERROR;
702    }
703
704    unsigned char aes_ivec[16];
705
706    AString iv;
707    if (itemMeta->findString("cipher-iv", &iv)) {
708        if ((!iv.startsWith("0x") && !iv.startsWith("0X"))
709                || iv.size() != 16 * 2 + 2) {
710            LOGE("malformed cipher IV '%s'.", iv.c_str());
711            return ERROR_MALFORMED;
712        }
713
714        memset(aes_ivec, 0, sizeof(aes_ivec));
715        for (size_t i = 0; i < 16; ++i) {
716            char c1 = tolower(iv.c_str()[2 + 2 * i]);
717            char c2 = tolower(iv.c_str()[3 + 2 * i]);
718            if (!isxdigit(c1) || !isxdigit(c2)) {
719                LOGE("malformed cipher IV '%s'.", iv.c_str());
720                return ERROR_MALFORMED;
721            }
722            uint8_t nibble1 = isdigit(c1) ? c1 - '0' : c1 - 'a' + 10;
723            uint8_t nibble2 = isdigit(c2) ? c2 - '0' : c2 - 'a' + 10;
724
725            aes_ivec[i] = nibble1 << 4 | nibble2;
726        }
727    } else {
728        memset(aes_ivec, 0, sizeof(aes_ivec));
729        aes_ivec[15] = mSeqNumber & 0xff;
730        aes_ivec[14] = (mSeqNumber >> 8) & 0xff;
731        aes_ivec[13] = (mSeqNumber >> 16) & 0xff;
732        aes_ivec[12] = (mSeqNumber >> 24) & 0xff;
733    }
734
735    AES_cbc_encrypt(
736            buffer->data(), buffer->data(), buffer->size(),
737            &aes_key, aes_ivec, AES_DECRYPT);
738
739    // hexdump(buffer->data(), buffer->size());
740
741    size_t n = buffer->size();
742    CHECK_GT(n, 0u);
743
744    size_t pad = buffer->data()[n - 1];
745
746    CHECK_GT(pad, 0u);
747    CHECK_LE(pad, 16u);
748    CHECK_GE((size_t)n, pad);
749    for (size_t i = 0; i < pad; ++i) {
750        CHECK_EQ((unsigned)buffer->data()[n - 1 - i], pad);
751    }
752
753    n -= pad;
754
755    buffer->setRange(buffer->offset(), n);
756
757    return OK;
758}
759
760void LiveSession::postMonitorQueue(int64_t delayUs) {
761    sp<AMessage> msg = new AMessage(kWhatMonitorQueue, id());
762    msg->setInt32("generation", ++mMonitorQueueGeneration);
763    msg->post(delayUs);
764}
765
766void LiveSession::onSeek(const sp<AMessage> &msg) {
767    int64_t timeUs;
768    CHECK(msg->findInt64("timeUs", &timeUs));
769
770    mSeekTimeUs = timeUs;
771    postMonitorQueue();
772}
773
774status_t LiveSession::getDuration(int64_t *durationUs) {
775    Mutex::Autolock autoLock(mLock);
776    *durationUs = mDurationUs;
777
778    return OK;
779}
780
781bool LiveSession::isSeekable() {
782    int64_t durationUs;
783    return getDuration(&durationUs) == OK && durationUs >= 0;
784}
785
786}  // namespace android
787
788