1/*
2 * Copyright (C) 2013 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
17package android.media;
18
19import android.content.Context;
20import android.text.Layout.Alignment;
21import android.text.SpannableStringBuilder;
22import android.util.ArrayMap;
23import android.util.AttributeSet;
24import android.util.Log;
25import android.view.Gravity;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.accessibility.CaptioningManager;
29import android.view.accessibility.CaptioningManager.CaptionStyle;
30import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
31import android.widget.LinearLayout;
32
33import com.android.internal.widget.SubtitleView;
34
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.HashMap;
38import java.util.Map;
39import java.util.Vector;
40
41/** @hide */
42public class WebVttRenderer extends SubtitleController.Renderer {
43    private final Context mContext;
44
45    private WebVttRenderingWidget mRenderingWidget;
46
47    public WebVttRenderer(Context context) {
48        mContext = context;
49    }
50
51    @Override
52    public boolean supports(MediaFormat format) {
53        if (format.containsKey(MediaFormat.KEY_MIME)) {
54            return format.getString(MediaFormat.KEY_MIME).equals("text/vtt");
55        }
56        return false;
57    }
58
59    @Override
60    public SubtitleTrack createTrack(MediaFormat format) {
61        if (mRenderingWidget == null) {
62            mRenderingWidget = new WebVttRenderingWidget(mContext);
63        }
64
65        return new WebVttTrack(mRenderingWidget, format);
66    }
67}
68
69/** @hide */
70class TextTrackCueSpan {
71    long mTimestampMs;
72    boolean mEnabled;
73    String mText;
74    TextTrackCueSpan(String text, long timestamp) {
75        mTimestampMs = timestamp;
76        mText = text;
77        // spans with timestamp will be enabled by Cue.onTime
78        mEnabled = (mTimestampMs < 0);
79    }
80
81    @Override
82    public boolean equals(Object o) {
83        if (!(o instanceof TextTrackCueSpan)) {
84            return false;
85        }
86        TextTrackCueSpan span = (TextTrackCueSpan) o;
87        return mTimestampMs == span.mTimestampMs &&
88                mText.equals(span.mText);
89    }
90}
91
92/**
93 * @hide
94 *
95 * Extract all text without style, but with timestamp spans.
96 */
97class UnstyledTextExtractor implements Tokenizer.OnTokenListener {
98    StringBuilder mLine = new StringBuilder();
99    Vector<TextTrackCueSpan[]> mLines = new Vector<TextTrackCueSpan[]>();
100    Vector<TextTrackCueSpan> mCurrentLine = new Vector<TextTrackCueSpan>();
101    long mLastTimestamp;
102
103    UnstyledTextExtractor() {
104        init();
105    }
106
107    private void init() {
108        mLine.delete(0, mLine.length());
109        mLines.clear();
110        mCurrentLine.clear();
111        mLastTimestamp = -1;
112    }
113
114    @Override
115    public void onData(String s) {
116        mLine.append(s);
117    }
118
119    @Override
120    public void onStart(String tag, String[] classes, String annotation) { }
121
122    @Override
123    public void onEnd(String tag) { }
124
125    @Override
126    public void onTimeStamp(long timestampMs) {
127        // finish any prior span
128        if (mLine.length() > 0 && timestampMs != mLastTimestamp) {
129            mCurrentLine.add(
130                    new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
131            mLine.delete(0, mLine.length());
132        }
133        mLastTimestamp = timestampMs;
134    }
135
136    @Override
137    public void onLineEnd() {
138        // finish any pending span
139        if (mLine.length() > 0) {
140            mCurrentLine.add(
141                    new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
142            mLine.delete(0, mLine.length());
143        }
144
145        TextTrackCueSpan[] spans = new TextTrackCueSpan[mCurrentLine.size()];
146        mCurrentLine.toArray(spans);
147        mCurrentLine.clear();
148        mLines.add(spans);
149    }
150
151    public TextTrackCueSpan[][] getText() {
152        // for politeness, finish last cue-line if it ends abruptly
153        if (mLine.length() > 0 || mCurrentLine.size() > 0) {
154            onLineEnd();
155        }
156        TextTrackCueSpan[][] lines = new TextTrackCueSpan[mLines.size()][];
157        mLines.toArray(lines);
158        init();
159        return lines;
160    }
161}
162
163/**
164 * @hide
165 *
166 * Tokenizer tokenizes the WebVTT Cue Text into tags and data
167 */
168class Tokenizer {
169    private static final String TAG = "Tokenizer";
170    private TokenizerPhase mPhase;
171    private TokenizerPhase mDataTokenizer;
172    private TokenizerPhase mTagTokenizer;
173
174    private OnTokenListener mListener;
175    private String mLine;
176    private int mHandledLen;
177
178    interface TokenizerPhase {
179        TokenizerPhase start();
180        void tokenize();
181    }
182
183    class DataTokenizer implements TokenizerPhase {
184        // includes both WebVTT data && escape state
185        private StringBuilder mData;
186
187        public TokenizerPhase start() {
188            mData = new StringBuilder();
189            return this;
190        }
191
192        private boolean replaceEscape(String escape, String replacement, int pos) {
193            if (mLine.startsWith(escape, pos)) {
194                mData.append(mLine.substring(mHandledLen, pos));
195                mData.append(replacement);
196                mHandledLen = pos + escape.length();
197                pos = mHandledLen - 1;
198                return true;
199            }
200            return false;
201        }
202
203        @Override
204        public void tokenize() {
205            int end = mLine.length();
206            for (int pos = mHandledLen; pos < mLine.length(); pos++) {
207                if (mLine.charAt(pos) == '&') {
208                    if (replaceEscape("&amp;", "&", pos) ||
209                            replaceEscape("&lt;", "<", pos) ||
210                            replaceEscape("&gt;", ">", pos) ||
211                            replaceEscape("&lrm;", "\u200e", pos) ||
212                            replaceEscape("&rlm;", "\u200f", pos) ||
213                            replaceEscape("&nbsp;", "\u00a0", pos)) {
214                        continue;
215                    }
216                } else if (mLine.charAt(pos) == '<') {
217                    end = pos;
218                    mPhase = mTagTokenizer.start();
219                    break;
220                }
221            }
222            mData.append(mLine.substring(mHandledLen, end));
223            // yield mData
224            mListener.onData(mData.toString());
225            mData.delete(0, mData.length());
226            mHandledLen = end;
227        }
228    }
229
230    class TagTokenizer implements TokenizerPhase {
231        private boolean mAtAnnotation;
232        private String mName, mAnnotation;
233
234        public TokenizerPhase start() {
235            mName = mAnnotation = "";
236            mAtAnnotation = false;
237            return this;
238        }
239
240        @Override
241        public void tokenize() {
242            if (!mAtAnnotation)
243                mHandledLen++;
244            if (mHandledLen < mLine.length()) {
245                String[] parts;
246                /**
247                 * Collect annotations and end-tags to closing >.  Collect tag
248                 * name to closing bracket or next white-space.
249                 */
250                if (mAtAnnotation || mLine.charAt(mHandledLen) == '/') {
251                    parts = mLine.substring(mHandledLen).split(">");
252                } else {
253                    parts = mLine.substring(mHandledLen).split("[\t\f >]");
254                }
255                String part = mLine.substring(
256                            mHandledLen, mHandledLen + parts[0].length());
257                mHandledLen += parts[0].length();
258
259                if (mAtAnnotation) {
260                    mAnnotation += " " + part;
261                } else {
262                    mName = part;
263                }
264            }
265
266            mAtAnnotation = true;
267
268            if (mHandledLen < mLine.length() && mLine.charAt(mHandledLen) == '>') {
269                yield_tag();
270                mPhase = mDataTokenizer.start();
271                mHandledLen++;
272            }
273        }
274
275        private void yield_tag() {
276            if (mName.startsWith("/")) {
277                mListener.onEnd(mName.substring(1));
278            } else if (mName.length() > 0 && Character.isDigit(mName.charAt(0))) {
279                // timestamp
280                try {
281                    long timestampMs = WebVttParser.parseTimestampMs(mName);
282                    mListener.onTimeStamp(timestampMs);
283                } catch (NumberFormatException e) {
284                    Log.d(TAG, "invalid timestamp tag: <" + mName + ">");
285                }
286            } else {
287                mAnnotation = mAnnotation.replaceAll("\\s+", " ");
288                if (mAnnotation.startsWith(" ")) {
289                    mAnnotation = mAnnotation.substring(1);
290                }
291                if (mAnnotation.endsWith(" ")) {
292                    mAnnotation = mAnnotation.substring(0, mAnnotation.length() - 1);
293                }
294
295                String[] classes = null;
296                int dotAt = mName.indexOf('.');
297                if (dotAt >= 0) {
298                    classes = mName.substring(dotAt + 1).split("\\.");
299                    mName = mName.substring(0, dotAt);
300                }
301                mListener.onStart(mName, classes, mAnnotation);
302            }
303        }
304    }
305
306    Tokenizer(OnTokenListener listener) {
307        mDataTokenizer = new DataTokenizer();
308        mTagTokenizer = new TagTokenizer();
309        reset();
310        mListener = listener;
311    }
312
313    void reset() {
314        mPhase = mDataTokenizer.start();
315    }
316
317    void tokenize(String s) {
318        mHandledLen = 0;
319        mLine = s;
320        while (mHandledLen < mLine.length()) {
321            mPhase.tokenize();
322        }
323        /* we are finished with a line unless we are in the middle of a tag */
324        if (!(mPhase instanceof TagTokenizer)) {
325            // yield END-OF-LINE
326            mListener.onLineEnd();
327        }
328    }
329
330    interface OnTokenListener {
331        void onData(String s);
332        void onStart(String tag, String[] classes, String annotation);
333        void onEnd(String tag);
334        void onTimeStamp(long timestampMs);
335        void onLineEnd();
336    }
337}
338
339/** @hide */
340class TextTrackRegion {
341    final static int SCROLL_VALUE_NONE      = 300;
342    final static int SCROLL_VALUE_SCROLL_UP = 301;
343
344    String mId;
345    float mWidth;
346    int mLines;
347    float mAnchorPointX, mAnchorPointY;
348    float mViewportAnchorPointX, mViewportAnchorPointY;
349    int mScrollValue;
350
351    TextTrackRegion() {
352        mId = "";
353        mWidth = 100;
354        mLines = 3;
355        mAnchorPointX = mViewportAnchorPointX = 0.f;
356        mAnchorPointY = mViewportAnchorPointY = 100.f;
357        mScrollValue = SCROLL_VALUE_NONE;
358    }
359
360    public String toString() {
361        StringBuilder res = new StringBuilder(" {id:\"").append(mId)
362            .append("\", width:").append(mWidth)
363            .append(", lines:").append(mLines)
364            .append(", anchorPoint:(").append(mAnchorPointX)
365            .append(", ").append(mAnchorPointY)
366            .append("), viewportAnchorPoints:").append(mViewportAnchorPointX)
367            .append(", ").append(mViewportAnchorPointY)
368            .append("), scrollValue:")
369            .append(mScrollValue == SCROLL_VALUE_NONE ? "none" :
370                    mScrollValue == SCROLL_VALUE_SCROLL_UP ? "scroll_up" :
371                    "INVALID")
372            .append("}");
373        return res.toString();
374    }
375}
376
377/** @hide */
378class TextTrackCue extends SubtitleTrack.Cue {
379    final static int WRITING_DIRECTION_HORIZONTAL  = 100;
380    final static int WRITING_DIRECTION_VERTICAL_RL = 101;
381    final static int WRITING_DIRECTION_VERTICAL_LR = 102;
382
383    final static int ALIGNMENT_MIDDLE = 200;
384    final static int ALIGNMENT_START  = 201;
385    final static int ALIGNMENT_END    = 202;
386    final static int ALIGNMENT_LEFT   = 203;
387    final static int ALIGNMENT_RIGHT  = 204;
388    private static final String TAG = "TTCue";
389
390    String  mId;
391    boolean mPauseOnExit;
392    int     mWritingDirection;
393    String  mRegionId;
394    boolean mSnapToLines;
395    Integer mLinePosition;  // null means AUTO
396    boolean mAutoLinePosition;
397    int     mTextPosition;
398    int     mSize;
399    int     mAlignment;
400    // Vector<String> mText;
401    String[] mStrings;
402    TextTrackCueSpan[][] mLines;
403    TextTrackRegion mRegion;
404
405    TextTrackCue() {
406        mId = "";
407        mPauseOnExit = false;
408        mWritingDirection = WRITING_DIRECTION_HORIZONTAL;
409        mRegionId = "";
410        mSnapToLines = true;
411        mLinePosition = null /* AUTO */;
412        mTextPosition = 50;
413        mSize = 100;
414        mAlignment = ALIGNMENT_MIDDLE;
415        mLines = null;
416        mRegion = null;
417    }
418
419    @Override
420    public boolean equals(Object o) {
421        if (!(o instanceof TextTrackCue)) {
422            return false;
423        }
424        if (this == o) {
425            return true;
426        }
427
428        try {
429            TextTrackCue cue = (TextTrackCue) o;
430            boolean res = mId.equals(cue.mId) &&
431                    mPauseOnExit == cue.mPauseOnExit &&
432                    mWritingDirection == cue.mWritingDirection &&
433                    mRegionId.equals(cue.mRegionId) &&
434                    mSnapToLines == cue.mSnapToLines &&
435                    mAutoLinePosition == cue.mAutoLinePosition &&
436                    (mAutoLinePosition ||
437                            ((mLinePosition != null && mLinePosition.equals(cue.mLinePosition)) ||
438                             (mLinePosition == null && cue.mLinePosition == null))) &&
439                    mTextPosition == cue.mTextPosition &&
440                    mSize == cue.mSize &&
441                    mAlignment == cue.mAlignment &&
442                    mLines.length == cue.mLines.length;
443            if (res == true) {
444                for (int line = 0; line < mLines.length; line++) {
445                    if (!Arrays.equals(mLines[line], cue.mLines[line])) {
446                        return false;
447                    }
448                }
449            }
450            return res;
451        } catch(IncompatibleClassChangeError e) {
452            return false;
453        }
454    }
455
456    public StringBuilder appendStringsToBuilder(StringBuilder builder) {
457        if (mStrings == null) {
458            builder.append("null");
459        } else {
460            builder.append("[");
461            boolean first = true;
462            for (String s: mStrings) {
463                if (!first) {
464                    builder.append(", ");
465                }
466                if (s == null) {
467                    builder.append("null");
468                } else {
469                    builder.append("\"");
470                    builder.append(s);
471                    builder.append("\"");
472                }
473                first = false;
474            }
475            builder.append("]");
476        }
477        return builder;
478    }
479
480    public StringBuilder appendLinesToBuilder(StringBuilder builder) {
481        if (mLines == null) {
482            builder.append("null");
483        } else {
484            builder.append("[");
485            boolean first = true;
486            for (TextTrackCueSpan[] spans: mLines) {
487                if (!first) {
488                    builder.append(", ");
489                }
490                if (spans == null) {
491                    builder.append("null");
492                } else {
493                    builder.append("\"");
494                    boolean innerFirst = true;
495                    long lastTimestamp = -1;
496                    for (TextTrackCueSpan span: spans) {
497                        if (!innerFirst) {
498                            builder.append(" ");
499                        }
500                        if (span.mTimestampMs != lastTimestamp) {
501                            builder.append("<")
502                                    .append(WebVttParser.timeToString(
503                                            span.mTimestampMs))
504                                    .append(">");
505                            lastTimestamp = span.mTimestampMs;
506                        }
507                        builder.append(span.mText);
508                        innerFirst = false;
509                    }
510                    builder.append("\"");
511                }
512                first = false;
513            }
514            builder.append("]");
515        }
516        return builder;
517    }
518
519    public String toString() {
520        StringBuilder res = new StringBuilder();
521
522        res.append(WebVttParser.timeToString(mStartTimeMs))
523                .append(" --> ").append(WebVttParser.timeToString(mEndTimeMs))
524                .append(" {id:\"").append(mId)
525                .append("\", pauseOnExit:").append(mPauseOnExit)
526                .append(", direction:")
527                .append(mWritingDirection == WRITING_DIRECTION_HORIZONTAL ? "horizontal" :
528                        mWritingDirection == WRITING_DIRECTION_VERTICAL_LR ? "vertical_lr" :
529                        mWritingDirection == WRITING_DIRECTION_VERTICAL_RL ? "vertical_rl" :
530                        "INVALID")
531                .append(", regionId:\"").append(mRegionId)
532                .append("\", snapToLines:").append(mSnapToLines)
533                .append(", linePosition:").append(mAutoLinePosition ? "auto" :
534                                                  mLinePosition)
535                .append(", textPosition:").append(mTextPosition)
536                .append(", size:").append(mSize)
537                .append(", alignment:")
538                .append(mAlignment == ALIGNMENT_END ? "end" :
539                        mAlignment == ALIGNMENT_LEFT ? "left" :
540                        mAlignment == ALIGNMENT_MIDDLE ? "middle" :
541                        mAlignment == ALIGNMENT_RIGHT ? "right" :
542                        mAlignment == ALIGNMENT_START ? "start" : "INVALID")
543                .append(", text:");
544        appendStringsToBuilder(res).append("}");
545        return res.toString();
546    }
547
548    @Override
549    public int hashCode() {
550        return toString().hashCode();
551    }
552
553    @Override
554    public void onTime(long timeMs) {
555        for (TextTrackCueSpan[] line: mLines) {
556            for (TextTrackCueSpan span: line) {
557                span.mEnabled = timeMs >= span.mTimestampMs;
558            }
559        }
560    }
561}
562
563/**
564 *  Supporting July 10 2013 draft version
565 *
566 *  @hide
567 */
568class WebVttParser {
569    private static final String TAG = "WebVttParser";
570    private Phase mPhase;
571    private TextTrackCue mCue;
572    private Vector<String> mCueTexts;
573    private WebVttCueListener mListener;
574    private String mBuffer;
575
576    WebVttParser(WebVttCueListener listener) {
577        mPhase = mParseStart;
578        mBuffer = "";   /* mBuffer contains up to 1 incomplete line */
579        mListener = listener;
580        mCueTexts = new Vector<String>();
581    }
582
583    /* parsePercentageString */
584    public static float parseFloatPercentage(String s)
585            throws NumberFormatException {
586        if (!s.endsWith("%")) {
587            throw new NumberFormatException("does not end in %");
588        }
589        s = s.substring(0, s.length() - 1);
590        // parseFloat allows an exponent or a sign
591        if (s.matches(".*[^0-9.].*")) {
592            throw new NumberFormatException("contains an invalid character");
593        }
594
595        try {
596            float value = Float.parseFloat(s);
597            if (value < 0.0f || value > 100.0f) {
598                throw new NumberFormatException("is out of range");
599            }
600            return value;
601        } catch (NumberFormatException e) {
602            throw new NumberFormatException("is not a number");
603        }
604    }
605
606    public static int parseIntPercentage(String s) throws NumberFormatException {
607        if (!s.endsWith("%")) {
608            throw new NumberFormatException("does not end in %");
609        }
610        s = s.substring(0, s.length() - 1);
611        // parseInt allows "-0" that returns 0, so check for non-digits
612        if (s.matches(".*[^0-9].*")) {
613            throw new NumberFormatException("contains an invalid character");
614        }
615
616        try {
617            int value = Integer.parseInt(s);
618            if (value < 0 || value > 100) {
619                throw new NumberFormatException("is out of range");
620            }
621            return value;
622        } catch (NumberFormatException e) {
623            throw new NumberFormatException("is not a number");
624        }
625    }
626
627    public static long parseTimestampMs(String s) throws NumberFormatException {
628        if (!s.matches("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}")) {
629            throw new NumberFormatException("has invalid format");
630        }
631
632        String[] parts = s.split("\\.", 2);
633        long value = 0;
634        for (String group: parts[0].split(":")) {
635            value = value * 60 + Long.parseLong(group);
636        }
637        return value * 1000 + Long.parseLong(parts[1]);
638    }
639
640    public static String timeToString(long timeMs) {
641        return String.format("%d:%02d:%02d.%03d",
642                timeMs / 3600000, (timeMs / 60000) % 60,
643                (timeMs / 1000) % 60, timeMs % 1000);
644    }
645
646    public void parse(String s) {
647        boolean trailingCR = false;
648        mBuffer = (mBuffer + s.replace("\0", "\ufffd")).replace("\r\n", "\n");
649
650        /* keep trailing '\r' in case matching '\n' arrives in next packet */
651        if (mBuffer.endsWith("\r")) {
652            trailingCR = true;
653            mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
654        }
655
656        String[] lines = mBuffer.split("[\r\n]");
657        for (int i = 0; i < lines.length - 1; i++) {
658            mPhase.parse(lines[i]);
659        }
660
661        mBuffer = lines[lines.length - 1];
662        if (trailingCR)
663            mBuffer += "\r";
664    }
665
666    public void eos() {
667        if (mBuffer.endsWith("\r")) {
668            mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
669        }
670
671        mPhase.parse(mBuffer);
672        mBuffer = "";
673
674        yieldCue();
675        mPhase = mParseStart;
676    }
677
678    public void yieldCue() {
679        if (mCue != null && mCueTexts.size() > 0) {
680            mCue.mStrings = new String[mCueTexts.size()];
681            mCueTexts.toArray(mCue.mStrings);
682            mCueTexts.clear();
683            mListener.onCueParsed(mCue);
684        }
685        mCue = null;
686    }
687
688    interface Phase {
689        void parse(String line);
690    }
691
692    final private Phase mSkipRest = new Phase() {
693        @Override
694        public void parse(String line) { }
695    };
696
697    final private Phase mParseStart = new Phase() { // 5-9
698        @Override
699        public void parse(String line) {
700            if (line.startsWith("\ufeff")) {
701                line = line.substring(1);
702            }
703            if (!line.equals("WEBVTT") &&
704                    !line.startsWith("WEBVTT ") &&
705                    !line.startsWith("WEBVTT\t")) {
706                log_warning("Not a WEBVTT header", line);
707                mPhase = mSkipRest;
708            } else {
709                mPhase = mParseHeader;
710            }
711        }
712    };
713
714    final private Phase mParseHeader = new Phase() { // 10-13
715        TextTrackRegion parseRegion(String s) {
716            TextTrackRegion region = new TextTrackRegion();
717            for (String setting: s.split(" +")) {
718                int equalAt = setting.indexOf('=');
719                if (equalAt <= 0 || equalAt == setting.length() - 1) {
720                    continue;
721                }
722
723                String name = setting.substring(0, equalAt);
724                String value = setting.substring(equalAt + 1);
725                if (name.equals("id")) {
726                    region.mId = value;
727                } else if (name.equals("width")) {
728                    try {
729                        region.mWidth = parseFloatPercentage(value);
730                    } catch (NumberFormatException e) {
731                        log_warning("region setting", name,
732                                "has invalid value", e.getMessage(), value);
733                    }
734                } else if (name.equals("lines")) {
735                    if (value.matches(".*[^0-9].*")) {
736                        log_warning("lines", name, "contains an invalid character", value);
737                    } else {
738                        try {
739                            region.mLines = Integer.parseInt(value);
740                            assert(region.mLines >= 0); // lines contains only digits
741                        } catch (NumberFormatException e) {
742                            log_warning("region setting", name, "is not numeric", value);
743                        }
744                    }
745                } else if (name.equals("regionanchor") ||
746                           name.equals("viewportanchor")) {
747                    int commaAt = value.indexOf(",");
748                    if (commaAt < 0) {
749                        log_warning("region setting", name, "contains no comma", value);
750                        continue;
751                    }
752
753                    String anchorX = value.substring(0, commaAt);
754                    String anchorY = value.substring(commaAt + 1);
755                    float x, y;
756
757                    try {
758                        x = parseFloatPercentage(anchorX);
759                    } catch (NumberFormatException e) {
760                        log_warning("region setting", name,
761                                "has invalid x component", e.getMessage(), anchorX);
762                        continue;
763                    }
764                    try {
765                        y = parseFloatPercentage(anchorY);
766                    } catch (NumberFormatException e) {
767                        log_warning("region setting", name,
768                                "has invalid y component", e.getMessage(), anchorY);
769                        continue;
770                    }
771
772                    if (name.charAt(0) == 'r') {
773                        region.mAnchorPointX = x;
774                        region.mAnchorPointY = y;
775                    } else {
776                        region.mViewportAnchorPointX = x;
777                        region.mViewportAnchorPointY = y;
778                    }
779                } else if (name.equals("scroll")) {
780                    if (value.equals("up")) {
781                        region.mScrollValue =
782                            TextTrackRegion.SCROLL_VALUE_SCROLL_UP;
783                    } else {
784                        log_warning("region setting", name, "has invalid value", value);
785                    }
786                }
787            }
788            return region;
789        }
790
791        @Override
792        public void parse(String line)  {
793            if (line.length() == 0) {
794                mPhase = mParseCueId;
795            } else if (line.contains("-->")) {
796                mPhase = mParseCueTime;
797                mPhase.parse(line);
798            } else {
799                int colonAt = line.indexOf(':');
800                if (colonAt <= 0 || colonAt >= line.length() - 1) {
801                    log_warning("meta data header has invalid format", line);
802                }
803                String name = line.substring(0, colonAt);
804                String value = line.substring(colonAt + 1);
805
806                if (name.equals("Region")) {
807                    TextTrackRegion region = parseRegion(value);
808                    mListener.onRegionParsed(region);
809                }
810            }
811        }
812    };
813
814    final private Phase mParseCueId = new Phase() {
815        @Override
816        public void parse(String line) {
817            if (line.length() == 0) {
818                return;
819            }
820
821            assert(mCue == null);
822
823            if (line.equals("NOTE") || line.startsWith("NOTE ")) {
824                mPhase = mParseCueText;
825            }
826
827            mCue = new TextTrackCue();
828            mCueTexts.clear();
829
830            mPhase = mParseCueTime;
831            if (line.contains("-->")) {
832                mPhase.parse(line);
833            } else {
834                mCue.mId = line;
835            }
836        }
837    };
838
839    final private Phase mParseCueTime = new Phase() {
840        @Override
841        public void parse(String line) {
842            int arrowAt = line.indexOf("-->");
843            if (arrowAt < 0) {
844                mCue = null;
845                mPhase = mParseCueId;
846                return;
847            }
848
849            String start = line.substring(0, arrowAt).trim();
850            // convert only initial and first other white-space to space
851            String rest = line.substring(arrowAt + 3)
852                    .replaceFirst("^\\s+", "").replaceFirst("\\s+", " ");
853            int spaceAt = rest.indexOf(' ');
854            String end = spaceAt > 0 ? rest.substring(0, spaceAt) : rest;
855            rest = spaceAt > 0 ? rest.substring(spaceAt + 1) : "";
856
857            mCue.mStartTimeMs = parseTimestampMs(start);
858            mCue.mEndTimeMs = parseTimestampMs(end);
859            for (String setting: rest.split(" +")) {
860                int colonAt = setting.indexOf(':');
861                if (colonAt <= 0 || colonAt == setting.length() - 1) {
862                    continue;
863                }
864                String name = setting.substring(0, colonAt);
865                String value = setting.substring(colonAt + 1);
866
867                if (name.equals("region")) {
868                    mCue.mRegionId = value;
869                } else if (name.equals("vertical")) {
870                    if (value.equals("rl")) {
871                        mCue.mWritingDirection =
872                            TextTrackCue.WRITING_DIRECTION_VERTICAL_RL;
873                    } else if (value.equals("lr")) {
874                        mCue.mWritingDirection =
875                            TextTrackCue.WRITING_DIRECTION_VERTICAL_LR;
876                    } else {
877                        log_warning("cue setting", name, "has invalid value", value);
878                    }
879                } else if (name.equals("line")) {
880                    try {
881                        /* TRICKY: we know that there are no spaces in value */
882                        assert(value.indexOf(' ') < 0);
883                        if (value.endsWith("%")) {
884                            mCue.mSnapToLines = false;
885                            mCue.mLinePosition = parseIntPercentage(value);
886                        } else if (value.matches(".*[^0-9].*")) {
887                            log_warning("cue setting", name,
888                                    "contains an invalid character", value);
889                        } else {
890                            mCue.mSnapToLines = true;
891                            mCue.mLinePosition = Integer.parseInt(value);
892                        }
893                    } catch (NumberFormatException e) {
894                        log_warning("cue setting", name,
895                                "is not numeric or percentage", value);
896                    }
897                    // TODO: add support for optional alignment value [,start|middle|end]
898                } else if (name.equals("position")) {
899                    try {
900                        mCue.mTextPosition = parseIntPercentage(value);
901                    } catch (NumberFormatException e) {
902                        log_warning("cue setting", name,
903                               "is not numeric or percentage", value);
904                    }
905                } else if (name.equals("size")) {
906                    try {
907                        mCue.mSize = parseIntPercentage(value);
908                    } catch (NumberFormatException e) {
909                        log_warning("cue setting", name,
910                               "is not numeric or percentage", value);
911                    }
912                } else if (name.equals("align")) {
913                    if (value.equals("start")) {
914                        mCue.mAlignment = TextTrackCue.ALIGNMENT_START;
915                    } else if (value.equals("middle")) {
916                        mCue.mAlignment = TextTrackCue.ALIGNMENT_MIDDLE;
917                    } else if (value.equals("end")) {
918                        mCue.mAlignment = TextTrackCue.ALIGNMENT_END;
919                    } else if (value.equals("left")) {
920                        mCue.mAlignment = TextTrackCue.ALIGNMENT_LEFT;
921                    } else if (value.equals("right")) {
922                        mCue.mAlignment = TextTrackCue.ALIGNMENT_RIGHT;
923                    } else {
924                        log_warning("cue setting", name, "has invalid value", value);
925                        continue;
926                    }
927                }
928            }
929
930            if (mCue.mLinePosition != null ||
931                    mCue.mSize != 100 ||
932                    (mCue.mWritingDirection !=
933                        TextTrackCue.WRITING_DIRECTION_HORIZONTAL)) {
934                mCue.mRegionId = "";
935            }
936
937            mPhase = mParseCueText;
938        }
939    };
940
941    /* also used for notes */
942    final private Phase mParseCueText = new Phase() {
943        @Override
944        public void parse(String line) {
945            if (line.length() == 0) {
946                yieldCue();
947                mPhase = mParseCueId;
948                return;
949            } else if (mCue != null) {
950                mCueTexts.add(line);
951            }
952        }
953    };
954
955    private void log_warning(
956            String nameType, String name, String message,
957            String subMessage, String value) {
958        Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
959                message + " ('" + value + "' " + subMessage + ")");
960    }
961
962    private void log_warning(
963            String nameType, String name, String message, String value) {
964        Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
965                message + " ('" + value + "')");
966    }
967
968    private void log_warning(String message, String value) {
969        Log.w(this.getClass().getName(), message + " ('" + value + "')");
970    }
971}
972
973/** @hide */
974interface WebVttCueListener {
975    void onCueParsed(TextTrackCue cue);
976    void onRegionParsed(TextTrackRegion region);
977}
978
979/** @hide */
980class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
981    private static final String TAG = "WebVttTrack";
982
983    private final WebVttParser mParser = new WebVttParser(this);
984    private final UnstyledTextExtractor mExtractor =
985        new UnstyledTextExtractor();
986    private final Tokenizer mTokenizer = new Tokenizer(mExtractor);
987    private final Vector<Long> mTimestamps = new Vector<Long>();
988    private final WebVttRenderingWidget mRenderingWidget;
989
990    private final Map<String, TextTrackRegion> mRegions =
991        new HashMap<String, TextTrackRegion>();
992    private Long mCurrentRunID;
993
994    WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
995        super(format);
996
997        mRenderingWidget = renderingWidget;
998    }
999
1000    @Override
1001    public WebVttRenderingWidget getRenderingWidget() {
1002        return mRenderingWidget;
1003    }
1004
1005    @Override
1006    public void onData(byte[] data, boolean eos, long runID) {
1007        try {
1008            String str = new String(data, "UTF-8");
1009
1010            // implement intermixing restriction for WebVTT only for now
1011            synchronized(mParser) {
1012                if (mCurrentRunID != null && runID != mCurrentRunID) {
1013                    throw new IllegalStateException(
1014                            "Run #" + mCurrentRunID +
1015                            " in progress.  Cannot process run #" + runID);
1016                }
1017                mCurrentRunID = runID;
1018                mParser.parse(str);
1019                if (eos) {
1020                    finishedRun(runID);
1021                    mParser.eos();
1022                    mRegions.clear();
1023                    mCurrentRunID = null;
1024                }
1025            }
1026        } catch (java.io.UnsupportedEncodingException e) {
1027            Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
1028        }
1029    }
1030
1031    @Override
1032    public void onCueParsed(TextTrackCue cue) {
1033        synchronized (mParser) {
1034            // resolve region
1035            if (cue.mRegionId.length() != 0) {
1036                cue.mRegion = mRegions.get(cue.mRegionId);
1037            }
1038
1039            if (DEBUG) Log.v(TAG, "adding cue " + cue);
1040
1041            // tokenize text track string-lines into lines of spans
1042            mTokenizer.reset();
1043            for (String s: cue.mStrings) {
1044                mTokenizer.tokenize(s);
1045            }
1046            cue.mLines = mExtractor.getText();
1047            if (DEBUG) Log.v(TAG, cue.appendLinesToBuilder(
1048                    cue.appendStringsToBuilder(
1049                        new StringBuilder()).append(" simplified to: "))
1050                            .toString());
1051
1052            // extract inner timestamps
1053            for (TextTrackCueSpan[] line: cue.mLines) {
1054                for (TextTrackCueSpan span: line) {
1055                    if (span.mTimestampMs > cue.mStartTimeMs &&
1056                            span.mTimestampMs < cue.mEndTimeMs &&
1057                            !mTimestamps.contains(span.mTimestampMs)) {
1058                        mTimestamps.add(span.mTimestampMs);
1059                    }
1060                }
1061            }
1062
1063            if (mTimestamps.size() > 0) {
1064                cue.mInnerTimesMs = new long[mTimestamps.size()];
1065                for (int ix=0; ix < mTimestamps.size(); ++ix) {
1066                    cue.mInnerTimesMs[ix] = mTimestamps.get(ix);
1067                }
1068                mTimestamps.clear();
1069            } else {
1070                cue.mInnerTimesMs = null;
1071            }
1072
1073            cue.mRunID = mCurrentRunID;
1074        }
1075
1076        addCue(cue);
1077    }
1078
1079    @Override
1080    public void onRegionParsed(TextTrackRegion region) {
1081        synchronized(mParser) {
1082            mRegions.put(region.mId, region);
1083        }
1084    }
1085
1086    @Override
1087    public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
1088        if (!mVisible) {
1089            // don't keep the state if we are not visible
1090            return;
1091        }
1092
1093        if (DEBUG && mTimeProvider != null) {
1094            try {
1095                Log.d(TAG, "at " +
1096                        (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
1097                        " ms the active cues are:");
1098            } catch (IllegalStateException e) {
1099                Log.d(TAG, "at (illegal state) the active cues are:");
1100            }
1101        }
1102
1103        if (mRenderingWidget != null) {
1104            mRenderingWidget.setActiveCues(activeCues);
1105        }
1106    }
1107}
1108
1109/**
1110 * Widget capable of rendering WebVTT captions.
1111 *
1112 * @hide
1113 */
1114class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
1115    private static final boolean DEBUG = false;
1116
1117    private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;
1118
1119    private static final int DEBUG_REGION_BACKGROUND = 0x800000FF;
1120    private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000;
1121
1122    /** WebVtt specifies line height as 5.3% of the viewport height. */
1123    private static final float LINE_HEIGHT_RATIO = 0.0533f;
1124
1125    /** Map of active regions, used to determine enter/exit. */
1126    private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes =
1127            new ArrayMap<TextTrackRegion, RegionLayout>();
1128
1129    /** Map of active cues, used to determine enter/exit. */
1130    private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes =
1131            new ArrayMap<TextTrackCue, CueLayout>();
1132
1133    /** Captioning manager, used to obtain and track caption properties. */
1134    private final CaptioningManager mManager;
1135
1136    /** Callback for rendering changes. */
1137    private OnChangedListener mListener;
1138
1139    /** Current caption style. */
1140    private CaptionStyle mCaptionStyle;
1141
1142    /** Current font size, computed from font scaling factor and height. */
1143    private float mFontSize;
1144
1145    /** Whether a caption style change listener is registered. */
1146    private boolean mHasChangeListener;
1147
1148    public WebVttRenderingWidget(Context context) {
1149        this(context, null);
1150    }
1151
1152    public WebVttRenderingWidget(Context context, AttributeSet attrs) {
1153        this(context, attrs, 0);
1154    }
1155
1156    public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
1157        this(context, attrs, defStyleAttr, 0);
1158    }
1159
1160    public WebVttRenderingWidget(
1161            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
1162        super(context, attrs, defStyleAttr, defStyleRes);
1163
1164        // Cannot render text over video when layer type is hardware.
1165        setLayerType(View.LAYER_TYPE_SOFTWARE, null);
1166
1167        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
1168        mCaptionStyle = mManager.getUserStyle();
1169        mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
1170    }
1171
1172    @Override
1173    public void setSize(int width, int height) {
1174        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
1175        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
1176
1177        measure(widthSpec, heightSpec);
1178        layout(0, 0, width, height);
1179    }
1180
1181    @Override
1182    public void onAttachedToWindow() {
1183        super.onAttachedToWindow();
1184
1185        manageChangeListener();
1186    }
1187
1188    @Override
1189    public void onDetachedFromWindow() {
1190        super.onDetachedFromWindow();
1191
1192        manageChangeListener();
1193    }
1194
1195    @Override
1196    public void setOnChangedListener(OnChangedListener listener) {
1197        mListener = listener;
1198    }
1199
1200    @Override
1201    public void setVisible(boolean visible) {
1202        if (visible) {
1203            setVisibility(View.VISIBLE);
1204        } else {
1205            setVisibility(View.GONE);
1206        }
1207
1208        manageChangeListener();
1209    }
1210
1211    /**
1212     * Manages whether this renderer is listening for caption style changes.
1213     */
1214    private void manageChangeListener() {
1215        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
1216        if (mHasChangeListener != needsListener) {
1217            mHasChangeListener = needsListener;
1218
1219            if (needsListener) {
1220                mManager.addCaptioningChangeListener(mCaptioningListener);
1221
1222                final CaptionStyle captionStyle = mManager.getUserStyle();
1223                final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
1224                setCaptionStyle(captionStyle, fontSize);
1225            } else {
1226                mManager.removeCaptioningChangeListener(mCaptioningListener);
1227            }
1228        }
1229    }
1230
1231    public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
1232        final Context context = getContext();
1233        final CaptionStyle captionStyle = mCaptionStyle;
1234        final float fontSize = mFontSize;
1235
1236        prepForPrune();
1237
1238        // Ensure we have all necessary cue and region boxes.
1239        final int count = activeCues.size();
1240        for (int i = 0; i < count; i++) {
1241            final TextTrackCue cue = (TextTrackCue) activeCues.get(i);
1242            final TextTrackRegion region = cue.mRegion;
1243            if (region != null) {
1244                RegionLayout regionBox = mRegionBoxes.get(region);
1245                if (regionBox == null) {
1246                    regionBox = new RegionLayout(context, region, captionStyle, fontSize);
1247                    mRegionBoxes.put(region, regionBox);
1248                    addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
1249                }
1250                regionBox.put(cue);
1251            } else {
1252                CueLayout cueBox = mCueBoxes.get(cue);
1253                if (cueBox == null) {
1254                    cueBox = new CueLayout(context, cue, captionStyle, fontSize);
1255                    mCueBoxes.put(cue, cueBox);
1256                    addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
1257                }
1258                cueBox.update();
1259                cueBox.setOrder(i);
1260            }
1261        }
1262
1263        prune();
1264
1265        // Force measurement and layout.
1266        final int width = getWidth();
1267        final int height = getHeight();
1268        setSize(width, height);
1269
1270        if (mListener != null) {
1271            mListener.onChanged(this);
1272        }
1273    }
1274
1275    private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
1276        captionStyle = DEFAULT_CAPTION_STYLE.applyStyle(captionStyle);
1277        mCaptionStyle = captionStyle;
1278        mFontSize = fontSize;
1279
1280        final int cueCount = mCueBoxes.size();
1281        for (int i = 0; i < cueCount; i++) {
1282            final CueLayout cueBox = mCueBoxes.valueAt(i);
1283            cueBox.setCaptionStyle(captionStyle, fontSize);
1284        }
1285
1286        final int regionCount = mRegionBoxes.size();
1287        for (int i = 0; i < regionCount; i++) {
1288            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
1289            regionBox.setCaptionStyle(captionStyle, fontSize);
1290        }
1291    }
1292
1293    /**
1294     * Remove inactive cues and regions.
1295     */
1296    private void prune() {
1297        int regionCount = mRegionBoxes.size();
1298        for (int i = 0; i < regionCount; i++) {
1299            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
1300            if (regionBox.prune()) {
1301                removeView(regionBox);
1302                mRegionBoxes.removeAt(i);
1303                regionCount--;
1304                i--;
1305            }
1306        }
1307
1308        int cueCount = mCueBoxes.size();
1309        for (int i = 0; i < cueCount; i++) {
1310            final CueLayout cueBox = mCueBoxes.valueAt(i);
1311            if (!cueBox.isActive()) {
1312                removeView(cueBox);
1313                mCueBoxes.removeAt(i);
1314                cueCount--;
1315                i--;
1316            }
1317        }
1318    }
1319
1320    /**
1321     * Reset active cues and regions.
1322     */
1323    private void prepForPrune() {
1324        final int regionCount = mRegionBoxes.size();
1325        for (int i = 0; i < regionCount; i++) {
1326            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
1327            regionBox.prepForPrune();
1328        }
1329
1330        final int cueCount = mCueBoxes.size();
1331        for (int i = 0; i < cueCount; i++) {
1332            final CueLayout cueBox = mCueBoxes.valueAt(i);
1333            cueBox.prepForPrune();
1334        }
1335    }
1336
1337    @Override
1338    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1339        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1340
1341        final int regionCount = mRegionBoxes.size();
1342        for (int i = 0; i < regionCount; i++) {
1343            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
1344            regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
1345        }
1346
1347        final int cueCount = mCueBoxes.size();
1348        for (int i = 0; i < cueCount; i++) {
1349            final CueLayout cueBox = mCueBoxes.valueAt(i);
1350            cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
1351        }
1352    }
1353
1354    @Override
1355    protected void onLayout(boolean changed, int l, int t, int r, int b) {
1356        final int viewportWidth = r - l;
1357        final int viewportHeight = b - t;
1358
1359        setCaptionStyle(mCaptionStyle,
1360                mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight);
1361
1362        final int regionCount = mRegionBoxes.size();
1363        for (int i = 0; i < regionCount; i++) {
1364            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
1365            layoutRegion(viewportWidth, viewportHeight, regionBox);
1366        }
1367
1368        final int cueCount = mCueBoxes.size();
1369        for (int i = 0; i < cueCount; i++) {
1370            final CueLayout cueBox = mCueBoxes.valueAt(i);
1371            layoutCue(viewportWidth, viewportHeight, cueBox);
1372        }
1373    }
1374
1375    /**
1376     * Lays out a region within the viewport. The region handles layout for
1377     * contained cues.
1378     */
1379    private void layoutRegion(
1380            int viewportWidth, int viewportHeight,
1381            RegionLayout regionBox) {
1382        final TextTrackRegion region = regionBox.getRegion();
1383        final int regionHeight = regionBox.getMeasuredHeight();
1384        final int regionWidth = regionBox.getMeasuredWidth();
1385
1386        // TODO: Account for region anchor point.
1387        final float x = region.mViewportAnchorPointX;
1388        final float y = region.mViewportAnchorPointY;
1389        final int left = (int) (x * (viewportWidth - regionWidth) / 100);
1390        final int top = (int) (y * (viewportHeight - regionHeight) / 100);
1391
1392        regionBox.layout(left, top, left + regionWidth, top + regionHeight);
1393    }
1394
1395    /**
1396     * Lays out a cue within the viewport.
1397     */
1398    private void layoutCue(
1399            int viewportWidth, int viewportHeight, CueLayout cueBox) {
1400        final TextTrackCue cue = cueBox.getCue();
1401        final int direction = getLayoutDirection();
1402        final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
1403        final boolean cueSnapToLines = cue.mSnapToLines;
1404
1405        int size = 100 * cueBox.getMeasuredWidth() / viewportWidth;
1406
1407        // Determine raw x-position.
1408        int xPosition;
1409        switch (absAlignment) {
1410            case TextTrackCue.ALIGNMENT_LEFT:
1411                xPosition = cue.mTextPosition;
1412                break;
1413            case TextTrackCue.ALIGNMENT_RIGHT:
1414                xPosition = cue.mTextPosition - size;
1415                break;
1416            case TextTrackCue.ALIGNMENT_MIDDLE:
1417            default:
1418                xPosition = cue.mTextPosition - size / 2;
1419                break;
1420        }
1421
1422        // Adjust x-position for layout.
1423        if (direction == LAYOUT_DIRECTION_RTL) {
1424            xPosition = 100 - xPosition;
1425        }
1426
1427        // If the text track cue snap-to-lines flag is set, adjust
1428        // x-position and size for padding. This is equivalent to placing the
1429        // cue within the title-safe area.
1430        if (cueSnapToLines) {
1431            final int paddingLeft = 100 * getPaddingLeft() / viewportWidth;
1432            final int paddingRight = 100 * getPaddingRight() / viewportWidth;
1433            if (xPosition < paddingLeft && xPosition + size > paddingLeft) {
1434                xPosition += paddingLeft;
1435                size -= paddingLeft;
1436            }
1437            final float rightEdge = 100 - paddingRight;
1438            if (xPosition < rightEdge && xPosition + size > rightEdge) {
1439                size -= paddingRight;
1440            }
1441        }
1442
1443        // Compute absolute left position and width.
1444        final int left = xPosition * viewportWidth / 100;
1445        final int width = size * viewportWidth / 100;
1446
1447        // Determine initial y-position.
1448        final int yPosition = calculateLinePosition(cueBox);
1449
1450        // Compute absolute final top position and height.
1451        final int height = cueBox.getMeasuredHeight();
1452        final int top;
1453        if (yPosition < 0) {
1454            // TODO: This needs to use the actual height of prior boxes.
1455            top = viewportHeight + yPosition * height;
1456        } else {
1457            top = yPosition * (viewportHeight - height) / 100;
1458        }
1459
1460        // Layout cue in final position.
1461        cueBox.layout(left, top, left + width, top + height);
1462    }
1463
1464    /**
1465     * Calculates the line position for a cue.
1466     * <p>
1467     * If the resulting position is negative, it represents a bottom-aligned
1468     * position relative to the number of active cues. Otherwise, it represents
1469     * a percentage [0-100] of the viewport height.
1470     */
1471    private int calculateLinePosition(CueLayout cueBox) {
1472        final TextTrackCue cue = cueBox.getCue();
1473        final Integer linePosition = cue.mLinePosition;
1474        final boolean snapToLines = cue.mSnapToLines;
1475        final boolean autoPosition = (linePosition == null);
1476
1477        if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) {
1478            // Invalid line position defaults to 100.
1479            return 100;
1480        } else if (!autoPosition) {
1481            // Use the valid, supplied line position.
1482            return linePosition;
1483        } else if (!snapToLines) {
1484            // Automatic, non-snapped line position defaults to 100.
1485            return 100;
1486        } else {
1487            // Automatic snapped line position uses active cue order.
1488            return -(cueBox.mOrder + 1);
1489        }
1490    }
1491
1492    /**
1493     * Resolves cue alignment according to the specified layout direction.
1494     */
1495    private static int resolveCueAlignment(int layoutDirection, int alignment) {
1496        switch (alignment) {
1497            case TextTrackCue.ALIGNMENT_START:
1498                return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
1499                        TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT;
1500            case TextTrackCue.ALIGNMENT_END:
1501                return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
1502                        TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT;
1503        }
1504        return alignment;
1505    }
1506
1507    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
1508        @Override
1509        public void onFontScaleChanged(float fontScale) {
1510            final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO;
1511            setCaptionStyle(mCaptionStyle, fontSize);
1512        }
1513
1514        @Override
1515        public void onUserStyleChanged(CaptionStyle userStyle) {
1516            setCaptionStyle(userStyle, mFontSize);
1517        }
1518    };
1519
1520    /**
1521     * A text track region represents a portion of the video viewport and
1522     * provides a rendering area for text track cues.
1523     */
1524    private static class RegionLayout extends LinearLayout {
1525        private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>();
1526        private final TextTrackRegion mRegion;
1527
1528        private CaptionStyle mCaptionStyle;
1529        private float mFontSize;
1530
1531        public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle,
1532                float fontSize) {
1533            super(context);
1534
1535            mRegion = region;
1536            mCaptionStyle = captionStyle;
1537            mFontSize = fontSize;
1538
1539            // TODO: Add support for vertical text
1540            setOrientation(VERTICAL);
1541
1542            if (DEBUG) {
1543                setBackgroundColor(DEBUG_REGION_BACKGROUND);
1544            } else {
1545                setBackgroundColor(captionStyle.windowColor);
1546            }
1547        }
1548
1549        public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
1550            mCaptionStyle = captionStyle;
1551            mFontSize = fontSize;
1552
1553            final int cueCount = mRegionCueBoxes.size();
1554            for (int i = 0; i < cueCount; i++) {
1555                final CueLayout cueBox = mRegionCueBoxes.get(i);
1556                cueBox.setCaptionStyle(captionStyle, fontSize);
1557            }
1558
1559            setBackgroundColor(captionStyle.windowColor);
1560        }
1561
1562        /**
1563         * Performs the parent's measurement responsibilities, then
1564         * automatically performs its own measurement.
1565         */
1566        public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
1567            final TextTrackRegion region = mRegion;
1568            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
1569            final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
1570            final int width = (int) region.mWidth;
1571
1572            // Determine the absolute maximum region size as the requested size.
1573            final int size = width * specWidth / 100;
1574
1575            widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
1576            heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
1577            measure(widthMeasureSpec, heightMeasureSpec);
1578        }
1579
1580        /**
1581         * Prepares this region for pruning by setting all tracks as inactive.
1582         * <p>
1583         * Tracks that are added or updated using {@link #put(TextTrackCue)}
1584         * after this calling this method will be marked as active.
1585         */
1586        public void prepForPrune() {
1587            final int cueCount = mRegionCueBoxes.size();
1588            for (int i = 0; i < cueCount; i++) {
1589                final CueLayout cueBox = mRegionCueBoxes.get(i);
1590                cueBox.prepForPrune();
1591            }
1592        }
1593
1594        /**
1595         * Adds a {@link TextTrackCue} to this region. If the track had already
1596         * been added, updates its active state.
1597         *
1598         * @param cue
1599         */
1600        public void put(TextTrackCue cue) {
1601            final int cueCount = mRegionCueBoxes.size();
1602            for (int i = 0; i < cueCount; i++) {
1603                final CueLayout cueBox = mRegionCueBoxes.get(i);
1604                if (cueBox.getCue() == cue) {
1605                    cueBox.update();
1606                    return;
1607                }
1608            }
1609
1610            final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize);
1611            mRegionCueBoxes.add(cueBox);
1612            addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
1613
1614            if (getChildCount() > mRegion.mLines) {
1615                removeViewAt(0);
1616            }
1617        }
1618
1619        /**
1620         * Remove all inactive tracks from this region.
1621         *
1622         * @return true if this region is empty and should be pruned
1623         */
1624        public boolean prune() {
1625            int cueCount = mRegionCueBoxes.size();
1626            for (int i = 0; i < cueCount; i++) {
1627                final CueLayout cueBox = mRegionCueBoxes.get(i);
1628                if (!cueBox.isActive()) {
1629                    mRegionCueBoxes.remove(i);
1630                    removeView(cueBox);
1631                    cueCount--;
1632                    i--;
1633                }
1634            }
1635
1636            return mRegionCueBoxes.isEmpty();
1637        }
1638
1639        /**
1640         * @return the region data backing this layout
1641         */
1642        public TextTrackRegion getRegion() {
1643            return mRegion;
1644        }
1645    }
1646
1647    /**
1648     * A text track cue is the unit of time-sensitive data in a text track,
1649     * corresponding for instance for subtitles and captions to the text that
1650     * appears at a particular time and disappears at another time.
1651     * <p>
1652     * A single cue may contain multiple {@link SpanLayout}s, each representing a
1653     * single line of text.
1654     */
1655    private static class CueLayout extends LinearLayout {
1656        public final TextTrackCue mCue;
1657
1658        private CaptionStyle mCaptionStyle;
1659        private float mFontSize;
1660
1661        private boolean mActive;
1662        private int mOrder;
1663
1664        public CueLayout(
1665                Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) {
1666            super(context);
1667
1668            mCue = cue;
1669            mCaptionStyle = captionStyle;
1670            mFontSize = fontSize;
1671
1672            // TODO: Add support for vertical text.
1673            final boolean horizontal = cue.mWritingDirection
1674                    == TextTrackCue.WRITING_DIRECTION_HORIZONTAL;
1675            setOrientation(horizontal ? VERTICAL : HORIZONTAL);
1676
1677            switch (cue.mAlignment) {
1678                case TextTrackCue.ALIGNMENT_END:
1679                    setGravity(Gravity.END);
1680                    break;
1681                case TextTrackCue.ALIGNMENT_LEFT:
1682                    setGravity(Gravity.LEFT);
1683                    break;
1684                case TextTrackCue.ALIGNMENT_MIDDLE:
1685                    setGravity(horizontal
1686                            ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
1687                    break;
1688                case TextTrackCue.ALIGNMENT_RIGHT:
1689                    setGravity(Gravity.RIGHT);
1690                    break;
1691                case TextTrackCue.ALIGNMENT_START:
1692                    setGravity(Gravity.START);
1693                    break;
1694            }
1695
1696            if (DEBUG) {
1697                setBackgroundColor(DEBUG_CUE_BACKGROUND);
1698            }
1699
1700            update();
1701        }
1702
1703        public void setCaptionStyle(CaptionStyle style, float fontSize) {
1704            mCaptionStyle = style;
1705            mFontSize = fontSize;
1706
1707            final int n = getChildCount();
1708            for (int i = 0; i < n; i++) {
1709                final View child = getChildAt(i);
1710                if (child instanceof SpanLayout) {
1711                    ((SpanLayout) child).setCaptionStyle(style, fontSize);
1712                }
1713            }
1714        }
1715
1716        public void prepForPrune() {
1717            mActive = false;
1718        }
1719
1720        public void update() {
1721            mActive = true;
1722
1723            removeAllViews();
1724
1725            final int cueAlignment = resolveCueAlignment(getLayoutDirection(), mCue.mAlignment);
1726            final Alignment alignment;
1727            switch (cueAlignment) {
1728                case TextTrackCue.ALIGNMENT_LEFT:
1729                    alignment = Alignment.ALIGN_LEFT;
1730                    break;
1731                case TextTrackCue.ALIGNMENT_RIGHT:
1732                    alignment = Alignment.ALIGN_RIGHT;
1733                    break;
1734                case TextTrackCue.ALIGNMENT_MIDDLE:
1735                default:
1736                    alignment = Alignment.ALIGN_CENTER;
1737            }
1738
1739            final CaptionStyle captionStyle = mCaptionStyle;
1740            final float fontSize = mFontSize;
1741            final TextTrackCueSpan[][] lines = mCue.mLines;
1742            final int lineCount = lines.length;
1743            for (int i = 0; i < lineCount; i++) {
1744                final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]);
1745                lineBox.setAlignment(alignment);
1746                lineBox.setCaptionStyle(captionStyle, fontSize);
1747
1748                addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
1749            }
1750        }
1751
1752        @Override
1753        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
1754            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
1755        }
1756
1757        /**
1758         * Performs the parent's measurement responsibilities, then
1759         * automatically performs its own measurement.
1760         */
1761        public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
1762            final TextTrackCue cue = mCue;
1763            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
1764            final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
1765            final int direction = getLayoutDirection();
1766            final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
1767
1768            // Determine the maximum size of cue based on its starting position
1769            // and the direction in which it grows.
1770            final int maximumSize;
1771            switch (absAlignment) {
1772                case TextTrackCue.ALIGNMENT_LEFT:
1773                    maximumSize = 100 - cue.mTextPosition;
1774                    break;
1775                case TextTrackCue.ALIGNMENT_RIGHT:
1776                    maximumSize = cue.mTextPosition;
1777                    break;
1778                case TextTrackCue.ALIGNMENT_MIDDLE:
1779                    if (cue.mTextPosition <= 50) {
1780                        maximumSize = cue.mTextPosition * 2;
1781                    } else {
1782                        maximumSize = (100 - cue.mTextPosition) * 2;
1783                    }
1784                    break;
1785                default:
1786                    maximumSize = 0;
1787            }
1788
1789            // Determine absolute maximum cue size as the smaller of the
1790            // requested size and the maximum theoretical size.
1791            final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100;
1792            widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
1793            heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
1794            measure(widthMeasureSpec, heightMeasureSpec);
1795        }
1796
1797        /**
1798         * Sets the order of this cue in the list of active cues.
1799         *
1800         * @param order the order of this cue in the list of active cues
1801         */
1802        public void setOrder(int order) {
1803            mOrder = order;
1804        }
1805
1806        /**
1807         * @return whether this cue is marked as active
1808         */
1809        public boolean isActive() {
1810            return mActive;
1811        }
1812
1813        /**
1814         * @return the cue data backing this layout
1815         */
1816        public TextTrackCue getCue() {
1817            return mCue;
1818        }
1819    }
1820
1821    /**
1822     * A text track line represents a single line of text within a cue.
1823     * <p>
1824     * A single line may contain multiple spans, each representing a section of
1825     * text that may be enabled or disabled at a particular time.
1826     */
1827    private static class SpanLayout extends SubtitleView {
1828        private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
1829        private final TextTrackCueSpan[] mSpans;
1830
1831        public SpanLayout(Context context, TextTrackCueSpan[] spans) {
1832            super(context);
1833
1834            mSpans = spans;
1835
1836            update();
1837        }
1838
1839        public void update() {
1840            final SpannableStringBuilder builder = mBuilder;
1841            final TextTrackCueSpan[] spans = mSpans;
1842
1843            builder.clear();
1844            builder.clearSpans();
1845
1846            final int spanCount = spans.length;
1847            for (int i = 0; i < spanCount; i++) {
1848                final TextTrackCueSpan span = spans[i];
1849                if (span.mEnabled) {
1850                    builder.append(spans[i].mText);
1851                }
1852            }
1853
1854            setText(builder);
1855        }
1856
1857        public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
1858            setBackgroundColor(captionStyle.backgroundColor);
1859            setForegroundColor(captionStyle.foregroundColor);
1860            setEdgeColor(captionStyle.edgeColor);
1861            setEdgeType(captionStyle.edgeType);
1862            setTypeface(captionStyle.getTypeface());
1863            setTextSize(fontSize);
1864        }
1865    }
1866}
1867