FileDocCategorySizeDatePackage
WebVttRenderer.javaAPI DocAndroid 5.1 API65676Thu Mar 12 22:22:30 GMT 2015android.media

WebVttRenderer.java

/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.media;

import android.content.Context;
import android.text.Layout.Alignment;
import android.text.SpannableStringBuilder;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.CaptioningManager;
import android.view.accessibility.CaptioningManager.CaptionStyle;
import android.view.accessibility.CaptioningManager.CaptioningChangeListener;
import android.widget.LinearLayout;

import com.android.internal.widget.SubtitleView;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;

/** @hide */
public class WebVttRenderer extends SubtitleController.Renderer {
    private final Context mContext;

    private WebVttRenderingWidget mRenderingWidget;

    public WebVttRenderer(Context context) {
        mContext = context;
    }

    @Override
    public boolean supports(MediaFormat format) {
        if (format.containsKey(MediaFormat.KEY_MIME)) {
            return format.getString(MediaFormat.KEY_MIME).equals("text/vtt");
        }
        return false;
    }

    @Override
    public SubtitleTrack createTrack(MediaFormat format) {
        if (mRenderingWidget == null) {
            mRenderingWidget = new WebVttRenderingWidget(mContext);
        }

        return new WebVttTrack(mRenderingWidget, format);
    }
}

/** @hide */
class TextTrackCueSpan {
    long mTimestampMs;
    boolean mEnabled;
    String mText;
    TextTrackCueSpan(String text, long timestamp) {
        mTimestampMs = timestamp;
        mText = text;
        // spans with timestamp will be enabled by Cue.onTime
        mEnabled = (mTimestampMs < 0);
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TextTrackCueSpan)) {
            return false;
        }
        TextTrackCueSpan span = (TextTrackCueSpan) o;
        return mTimestampMs == span.mTimestampMs &&
                mText.equals(span.mText);
    }
}

/**
 * @hide
 *
 * Extract all text without style, but with timestamp spans.
 */
class UnstyledTextExtractor implements Tokenizer.OnTokenListener {
    StringBuilder mLine = new StringBuilder();
    Vector<TextTrackCueSpan[]> mLines = new Vector<TextTrackCueSpan[]>();
    Vector<TextTrackCueSpan> mCurrentLine = new Vector<TextTrackCueSpan>();
    long mLastTimestamp;

    UnstyledTextExtractor() {
        init();
    }

    private void init() {
        mLine.delete(0, mLine.length());
        mLines.clear();
        mCurrentLine.clear();
        mLastTimestamp = -1;
    }

    @Override
    public void onData(String s) {
        mLine.append(s);
    }

    @Override
    public void onStart(String tag, String[] classes, String annotation) { }

    @Override
    public void onEnd(String tag) { }

    @Override
    public void onTimeStamp(long timestampMs) {
        // finish any prior span
        if (mLine.length() > 0 && timestampMs != mLastTimestamp) {
            mCurrentLine.add(
                    new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
            mLine.delete(0, mLine.length());
        }
        mLastTimestamp = timestampMs;
    }

    @Override
    public void onLineEnd() {
        // finish any pending span
        if (mLine.length() > 0) {
            mCurrentLine.add(
                    new TextTrackCueSpan(mLine.toString(), mLastTimestamp));
            mLine.delete(0, mLine.length());
        }

        TextTrackCueSpan[] spans = new TextTrackCueSpan[mCurrentLine.size()];
        mCurrentLine.toArray(spans);
        mCurrentLine.clear();
        mLines.add(spans);
    }

    public TextTrackCueSpan[][] getText() {
        // for politeness, finish last cue-line if it ends abruptly
        if (mLine.length() > 0 || mCurrentLine.size() > 0) {
            onLineEnd();
        }
        TextTrackCueSpan[][] lines = new TextTrackCueSpan[mLines.size()][];
        mLines.toArray(lines);
        init();
        return lines;
    }
}

/**
 * @hide
 *
 * Tokenizer tokenizes the WebVTT Cue Text into tags and data
 */
class Tokenizer {
    private static final String TAG = "Tokenizer";
    private TokenizerPhase mPhase;
    private TokenizerPhase mDataTokenizer;
    private TokenizerPhase mTagTokenizer;

    private OnTokenListener mListener;
    private String mLine;
    private int mHandledLen;

    interface TokenizerPhase {
        TokenizerPhase start();
        void tokenize();
    }

    class DataTokenizer implements TokenizerPhase {
        // includes both WebVTT data && escape state
        private StringBuilder mData;

        public TokenizerPhase start() {
            mData = new StringBuilder();
            return this;
        }

        private boolean replaceEscape(String escape, String replacement, int pos) {
            if (mLine.startsWith(escape, pos)) {
                mData.append(mLine.substring(mHandledLen, pos));
                mData.append(replacement);
                mHandledLen = pos + escape.length();
                pos = mHandledLen - 1;
                return true;
            }
            return false;
        }

        @Override
        public void tokenize() {
            int end = mLine.length();
            for (int pos = mHandledLen; pos < mLine.length(); pos++) {
                if (mLine.charAt(pos) == '&') {
                    if (replaceEscape("&", "&", pos) ||
                            replaceEscape("<", "<", pos) ||
                            replaceEscape(">", ">", pos) ||
                            replaceEscape("‎", "\u200e", pos) ||
                            replaceEscape("‏", "\u200f", pos) ||
                            replaceEscape(" ", "\u00a0", pos)) {
                        continue;
                    }
                } else if (mLine.charAt(pos) == '<') {
                    end = pos;
                    mPhase = mTagTokenizer.start();
                    break;
                }
            }
            mData.append(mLine.substring(mHandledLen, end));
            // yield mData
            mListener.onData(mData.toString());
            mData.delete(0, mData.length());
            mHandledLen = end;
        }
    }

    class TagTokenizer implements TokenizerPhase {
        private boolean mAtAnnotation;
        private String mName, mAnnotation;

        public TokenizerPhase start() {
            mName = mAnnotation = "";
            mAtAnnotation = false;
            return this;
        }

        @Override
        public void tokenize() {
            if (!mAtAnnotation)
                mHandledLen++;
            if (mHandledLen < mLine.length()) {
                String[] parts;
                /**
                 * Collect annotations and end-tags to closing >.  Collect tag
                 * name to closing bracket or next white-space.
                 */
                if (mAtAnnotation || mLine.charAt(mHandledLen) == '/') {
                    parts = mLine.substring(mHandledLen).split(">");
                } else {
                    parts = mLine.substring(mHandledLen).split("[\t\f >]");
                }
                String part = mLine.substring(
                            mHandledLen, mHandledLen + parts[0].length());
                mHandledLen += parts[0].length();

                if (mAtAnnotation) {
                    mAnnotation += " " + part;
                } else {
                    mName = part;
                }
            }

            mAtAnnotation = true;

            if (mHandledLen < mLine.length() && mLine.charAt(mHandledLen) == '>') {
                yield_tag();
                mPhase = mDataTokenizer.start();
                mHandledLen++;
            }
        }

        private void yield_tag() {
            if (mName.startsWith("/")) {
                mListener.onEnd(mName.substring(1));
            } else if (mName.length() > 0 && Character.isDigit(mName.charAt(0))) {
                // timestamp
                try {
                    long timestampMs = WebVttParser.parseTimestampMs(mName);
                    mListener.onTimeStamp(timestampMs);
                } catch (NumberFormatException e) {
                    Log.d(TAG, "invalid timestamp tag: <" + mName + ">");
                }
            } else {
                mAnnotation = mAnnotation.replaceAll("\\s+", " ");
                if (mAnnotation.startsWith(" ")) {
                    mAnnotation = mAnnotation.substring(1);
                }
                if (mAnnotation.endsWith(" ")) {
                    mAnnotation = mAnnotation.substring(0, mAnnotation.length() - 1);
                }

                String[] classes = null;
                int dotAt = mName.indexOf('.');
                if (dotAt >= 0) {
                    classes = mName.substring(dotAt + 1).split("\\.");
                    mName = mName.substring(0, dotAt);
                }
                mListener.onStart(mName, classes, mAnnotation);
            }
        }
    }

    Tokenizer(OnTokenListener listener) {
        mDataTokenizer = new DataTokenizer();
        mTagTokenizer = new TagTokenizer();
        reset();
        mListener = listener;
    }

    void reset() {
        mPhase = mDataTokenizer.start();
    }

    void tokenize(String s) {
        mHandledLen = 0;
        mLine = s;
        while (mHandledLen < mLine.length()) {
            mPhase.tokenize();
        }
        /* we are finished with a line unless we are in the middle of a tag */
        if (!(mPhase instanceof TagTokenizer)) {
            // yield END-OF-LINE
            mListener.onLineEnd();
        }
    }

    interface OnTokenListener {
        void onData(String s);
        void onStart(String tag, String[] classes, String annotation);
        void onEnd(String tag);
        void onTimeStamp(long timestampMs);
        void onLineEnd();
    }
}

/** @hide */
class TextTrackRegion {
    final static int SCROLL_VALUE_NONE      = 300;
    final static int SCROLL_VALUE_SCROLL_UP = 301;

    String mId;
    float mWidth;
    int mLines;
    float mAnchorPointX, mAnchorPointY;
    float mViewportAnchorPointX, mViewportAnchorPointY;
    int mScrollValue;

    TextTrackRegion() {
        mId = "";
        mWidth = 100;
        mLines = 3;
        mAnchorPointX = mViewportAnchorPointX = 0.f;
        mAnchorPointY = mViewportAnchorPointY = 100.f;
        mScrollValue = SCROLL_VALUE_NONE;
    }

    public String toString() {
        StringBuilder res = new StringBuilder(" {id:\"").append(mId)
            .append("\", width:").append(mWidth)
            .append(", lines:").append(mLines)
            .append(", anchorPoint:(").append(mAnchorPointX)
            .append(", ").append(mAnchorPointY)
            .append("), viewportAnchorPoints:").append(mViewportAnchorPointX)
            .append(", ").append(mViewportAnchorPointY)
            .append("), scrollValue:")
            .append(mScrollValue == SCROLL_VALUE_NONE ? "none" :
                    mScrollValue == SCROLL_VALUE_SCROLL_UP ? "scroll_up" :
                    "INVALID")
            .append("}");
        return res.toString();
    }
}

/** @hide */
class TextTrackCue extends SubtitleTrack.Cue {
    final static int WRITING_DIRECTION_HORIZONTAL  = 100;
    final static int WRITING_DIRECTION_VERTICAL_RL = 101;
    final static int WRITING_DIRECTION_VERTICAL_LR = 102;

    final static int ALIGNMENT_MIDDLE = 200;
    final static int ALIGNMENT_START  = 201;
    final static int ALIGNMENT_END    = 202;
    final static int ALIGNMENT_LEFT   = 203;
    final static int ALIGNMENT_RIGHT  = 204;
    private static final String TAG = "TTCue";

    String  mId;
    boolean mPauseOnExit;
    int     mWritingDirection;
    String  mRegionId;
    boolean mSnapToLines;
    Integer mLinePosition;  // null means AUTO
    boolean mAutoLinePosition;
    int     mTextPosition;
    int     mSize;
    int     mAlignment;
    // Vector<String> mText;
    String[] mStrings;
    TextTrackCueSpan[][] mLines;
    TextTrackRegion mRegion;

    TextTrackCue() {
        mId = "";
        mPauseOnExit = false;
        mWritingDirection = WRITING_DIRECTION_HORIZONTAL;
        mRegionId = "";
        mSnapToLines = true;
        mLinePosition = null /* AUTO */;
        mTextPosition = 50;
        mSize = 100;
        mAlignment = ALIGNMENT_MIDDLE;
        mLines = null;
        mRegion = null;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof TextTrackCue)) {
            return false;
        }
        if (this == o) {
            return true;
        }

        try {
            TextTrackCue cue = (TextTrackCue) o;
            boolean res = mId.equals(cue.mId) &&
                    mPauseOnExit == cue.mPauseOnExit &&
                    mWritingDirection == cue.mWritingDirection &&
                    mRegionId.equals(cue.mRegionId) &&
                    mSnapToLines == cue.mSnapToLines &&
                    mAutoLinePosition == cue.mAutoLinePosition &&
                    (mAutoLinePosition || mLinePosition == cue.mLinePosition) &&
                    mTextPosition == cue.mTextPosition &&
                    mSize == cue.mSize &&
                    mAlignment == cue.mAlignment &&
                    mLines.length == cue.mLines.length;
            if (res == true) {
                for (int line = 0; line < mLines.length; line++) {
                    if (!Arrays.equals(mLines[line], cue.mLines[line])) {
                        return false;
                    }
                }
            }
            return res;
        } catch(IncompatibleClassChangeError e) {
            return false;
        }
    }

    public StringBuilder appendStringsToBuilder(StringBuilder builder) {
        if (mStrings == null) {
            builder.append("null");
        } else {
            builder.append("[");
            boolean first = true;
            for (String s: mStrings) {
                if (!first) {
                    builder.append(", ");
                }
                if (s == null) {
                    builder.append("null");
                } else {
                    builder.append("\"");
                    builder.append(s);
                    builder.append("\"");
                }
                first = false;
            }
            builder.append("]");
        }
        return builder;
    }

    public StringBuilder appendLinesToBuilder(StringBuilder builder) {
        if (mLines == null) {
            builder.append("null");
        } else {
            builder.append("[");
            boolean first = true;
            for (TextTrackCueSpan[] spans: mLines) {
                if (!first) {
                    builder.append(", ");
                }
                if (spans == null) {
                    builder.append("null");
                } else {
                    builder.append("\"");
                    boolean innerFirst = true;
                    long lastTimestamp = -1;
                    for (TextTrackCueSpan span: spans) {
                        if (!innerFirst) {
                            builder.append(" ");
                        }
                        if (span.mTimestampMs != lastTimestamp) {
                            builder.append("<")
                                    .append(WebVttParser.timeToString(
                                            span.mTimestampMs))
                                    .append(">");
                            lastTimestamp = span.mTimestampMs;
                        }
                        builder.append(span.mText);
                        innerFirst = false;
                    }
                    builder.append("\"");
                }
                first = false;
            }
            builder.append("]");
        }
        return builder;
    }

    public String toString() {
        StringBuilder res = new StringBuilder();

        res.append(WebVttParser.timeToString(mStartTimeMs))
                .append(" --> ").append(WebVttParser.timeToString(mEndTimeMs))
                .append(" {id:\"").append(mId)
                .append("\", pauseOnExit:").append(mPauseOnExit)
                .append(", direction:")
                .append(mWritingDirection == WRITING_DIRECTION_HORIZONTAL ? "horizontal" :
                        mWritingDirection == WRITING_DIRECTION_VERTICAL_LR ? "vertical_lr" :
                        mWritingDirection == WRITING_DIRECTION_VERTICAL_RL ? "vertical_rl" :
                        "INVALID")
                .append(", regionId:\"").append(mRegionId)
                .append("\", snapToLines:").append(mSnapToLines)
                .append(", linePosition:").append(mAutoLinePosition ? "auto" :
                                                  mLinePosition)
                .append(", textPosition:").append(mTextPosition)
                .append(", size:").append(mSize)
                .append(", alignment:")
                .append(mAlignment == ALIGNMENT_END ? "end" :
                        mAlignment == ALIGNMENT_LEFT ? "left" :
                        mAlignment == ALIGNMENT_MIDDLE ? "middle" :
                        mAlignment == ALIGNMENT_RIGHT ? "right" :
                        mAlignment == ALIGNMENT_START ? "start" : "INVALID")
                .append(", text:");
        appendStringsToBuilder(res).append("}");
        return res.toString();
    }

    @Override
    public int hashCode() {
        return toString().hashCode();
    }

    @Override
    public void onTime(long timeMs) {
        for (TextTrackCueSpan[] line: mLines) {
            for (TextTrackCueSpan span: line) {
                span.mEnabled = timeMs >= span.mTimestampMs;
            }
        }
    }
}

/**
 *  Supporting July 10 2013 draft version
 *
 *  @hide
 */
class WebVttParser {
    private static final String TAG = "WebVttParser";
    private Phase mPhase;
    private TextTrackCue mCue;
    private Vector<String> mCueTexts;
    private WebVttCueListener mListener;
    private String mBuffer;

    WebVttParser(WebVttCueListener listener) {
        mPhase = mParseStart;
        mBuffer = "";   /* mBuffer contains up to 1 incomplete line */
        mListener = listener;
        mCueTexts = new Vector<String>();
    }

    /* parsePercentageString */
    public static float parseFloatPercentage(String s)
            throws NumberFormatException {
        if (!s.endsWith("%")) {
            throw new NumberFormatException("does not end in %");
        }
        s = s.substring(0, s.length() - 1);
        // parseFloat allows an exponent or a sign
        if (s.matches(".*[^0-9.].*")) {
            throw new NumberFormatException("contains an invalid character");
        }

        try {
            float value = Float.parseFloat(s);
            if (value < 0.0f || value > 100.0f) {
                throw new NumberFormatException("is out of range");
            }
            return value;
        } catch (NumberFormatException e) {
            throw new NumberFormatException("is not a number");
        }
    }

    public static int parseIntPercentage(String s) throws NumberFormatException {
        if (!s.endsWith("%")) {
            throw new NumberFormatException("does not end in %");
        }
        s = s.substring(0, s.length() - 1);
        // parseInt allows "-0" that returns 0, so check for non-digits
        if (s.matches(".*[^0-9].*")) {
            throw new NumberFormatException("contains an invalid character");
        }

        try {
            int value = Integer.parseInt(s);
            if (value < 0 || value > 100) {
                throw new NumberFormatException("is out of range");
            }
            return value;
        } catch (NumberFormatException e) {
            throw new NumberFormatException("is not a number");
        }
    }

    public static long parseTimestampMs(String s) throws NumberFormatException {
        if (!s.matches("(\\d+:)?[0-5]\\d:[0-5]\\d\\.\\d{3}")) {
            throw new NumberFormatException("has invalid format");
        }

        String[] parts = s.split("\\.", 2);
        long value = 0;
        for (String group: parts[0].split(":")) {
            value = value * 60 + Long.parseLong(group);
        }
        return value * 1000 + Long.parseLong(parts[1]);
    }

    public static String timeToString(long timeMs) {
        return String.format("%d:%02d:%02d.%03d",
                timeMs / 3600000, (timeMs / 60000) % 60,
                (timeMs / 1000) % 60, timeMs % 1000);
    }

    public void parse(String s) {
        boolean trailingCR = false;
        mBuffer = (mBuffer + s.replace("\0", "\ufffd")).replace("\r\n", "\n");

        /* keep trailing '\r' in case matching '\n' arrives in next packet */
        if (mBuffer.endsWith("\r")) {
            trailingCR = true;
            mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
        }

        String[] lines = mBuffer.split("[\r\n]");
        for (int i = 0; i < lines.length - 1; i++) {
            mPhase.parse(lines[i]);
        }

        mBuffer = lines[lines.length - 1];
        if (trailingCR)
            mBuffer += "\r";
    }

    public void eos() {
        if (mBuffer.endsWith("\r")) {
            mBuffer = mBuffer.substring(0, mBuffer.length() - 1);
        }

        mPhase.parse(mBuffer);
        mBuffer = "";

        yieldCue();
        mPhase = mParseStart;
    }

    public void yieldCue() {
        if (mCue != null && mCueTexts.size() > 0) {
            mCue.mStrings = new String[mCueTexts.size()];
            mCueTexts.toArray(mCue.mStrings);
            mCueTexts.clear();
            mListener.onCueParsed(mCue);
        }
        mCue = null;
    }

    interface Phase {
        void parse(String line);
    }

    final private Phase mSkipRest = new Phase() {
        @Override
        public void parse(String line) { }
    };

    final private Phase mParseStart = new Phase() { // 5-9
        @Override
        public void parse(String line) {
            if (line.startsWith("\ufeff")) {
                line = line.substring(1);
            }
            if (!line.equals("WEBVTT") &&
                    !line.startsWith("WEBVTT ") &&
                    !line.startsWith("WEBVTT\t")) {
                log_warning("Not a WEBVTT header", line);
                mPhase = mSkipRest;
            } else {
                mPhase = mParseHeader;
            }
        }
    };

    final private Phase mParseHeader = new Phase() { // 10-13
        TextTrackRegion parseRegion(String s) {
            TextTrackRegion region = new TextTrackRegion();
            for (String setting: s.split(" +")) {
                int equalAt = setting.indexOf('=');
                if (equalAt <= 0 || equalAt == setting.length() - 1) {
                    continue;
                }

                String name = setting.substring(0, equalAt);
                String value = setting.substring(equalAt + 1);
                if (name.equals("id")) {
                    region.mId = value;
                } else if (name.equals("width")) {
                    try {
                        region.mWidth = parseFloatPercentage(value);
                    } catch (NumberFormatException e) {
                        log_warning("region setting", name,
                                "has invalid value", e.getMessage(), value);
                    }
                } else if (name.equals("lines")) {
                    if (value.matches(".*[^0-9].*")) {
                        log_warning("lines", name, "contains an invalid character", value);
                    } else {
                        try {
                            region.mLines = Integer.parseInt(value);
                            assert(region.mLines >= 0); // lines contains only digits
                        } catch (NumberFormatException e) {
                            log_warning("region setting", name, "is not numeric", value);
                        }
                    }
                } else if (name.equals("regionanchor") ||
                           name.equals("viewportanchor")) {
                    int commaAt = value.indexOf(",");
                    if (commaAt < 0) {
                        log_warning("region setting", name, "contains no comma", value);
                        continue;
                    }

                    String anchorX = value.substring(0, commaAt);
                    String anchorY = value.substring(commaAt + 1);
                    float x, y;

                    try {
                        x = parseFloatPercentage(anchorX);
                    } catch (NumberFormatException e) {
                        log_warning("region setting", name,
                                "has invalid x component", e.getMessage(), anchorX);
                        continue;
                    }
                    try {
                        y = parseFloatPercentage(anchorY);
                    } catch (NumberFormatException e) {
                        log_warning("region setting", name,
                                "has invalid y component", e.getMessage(), anchorY);
                        continue;
                    }

                    if (name.charAt(0) == 'r') {
                        region.mAnchorPointX = x;
                        region.mAnchorPointY = y;
                    } else {
                        region.mViewportAnchorPointX = x;
                        region.mViewportAnchorPointY = y;
                    }
                } else if (name.equals("scroll")) {
                    if (value.equals("up")) {
                        region.mScrollValue =
                            TextTrackRegion.SCROLL_VALUE_SCROLL_UP;
                    } else {
                        log_warning("region setting", name, "has invalid value", value);
                    }
                }
            }
            return region;
        }

        @Override
        public void parse(String line)  {
            if (line.length() == 0) {
                mPhase = mParseCueId;
            } else if (line.contains("-->")) {
                mPhase = mParseCueTime;
                mPhase.parse(line);
            } else {
                int colonAt = line.indexOf(':');
                if (colonAt <= 0 || colonAt >= line.length() - 1) {
                    log_warning("meta data header has invalid format", line);
                }
                String name = line.substring(0, colonAt);
                String value = line.substring(colonAt + 1);

                if (name.equals("Region")) {
                    TextTrackRegion region = parseRegion(value);
                    mListener.onRegionParsed(region);
                }
            }
        }
    };

    final private Phase mParseCueId = new Phase() {
        @Override
        public void parse(String line) {
            if (line.length() == 0) {
                return;
            }

            assert(mCue == null);

            if (line.equals("NOTE") || line.startsWith("NOTE ")) {
                mPhase = mParseCueText;
            }

            mCue = new TextTrackCue();
            mCueTexts.clear();

            mPhase = mParseCueTime;
            if (line.contains("-->")) {
                mPhase.parse(line);
            } else {
                mCue.mId = line;
            }
        }
    };

    final private Phase mParseCueTime = new Phase() {
        @Override
        public void parse(String line) {
            int arrowAt = line.indexOf("-->");
            if (arrowAt < 0) {
                mCue = null;
                mPhase = mParseCueId;
                return;
            }

            String start = line.substring(0, arrowAt).trim();
            // convert only initial and first other white-space to space
            String rest = line.substring(arrowAt + 3)
                    .replaceFirst("^\\s+", "").replaceFirst("\\s+", " ");
            int spaceAt = rest.indexOf(' ');
            String end = spaceAt > 0 ? rest.substring(0, spaceAt) : rest;
            rest = spaceAt > 0 ? rest.substring(spaceAt + 1) : "";

            mCue.mStartTimeMs = parseTimestampMs(start);
            mCue.mEndTimeMs = parseTimestampMs(end);
            for (String setting: rest.split(" +")) {
                int colonAt = setting.indexOf(':');
                if (colonAt <= 0 || colonAt == setting.length() - 1) {
                    continue;
                }
                String name = setting.substring(0, colonAt);
                String value = setting.substring(colonAt + 1);

                if (name.equals("region")) {
                    mCue.mRegionId = value;
                } else if (name.equals("vertical")) {
                    if (value.equals("rl")) {
                        mCue.mWritingDirection =
                            TextTrackCue.WRITING_DIRECTION_VERTICAL_RL;
                    } else if (value.equals("lr")) {
                        mCue.mWritingDirection =
                            TextTrackCue.WRITING_DIRECTION_VERTICAL_LR;
                    } else {
                        log_warning("cue setting", name, "has invalid value", value);
                    }
                } else if (name.equals("line")) {
                    try {
                        /* TRICKY: we know that there are no spaces in value */
                        assert(value.indexOf(' ') < 0);
                        if (value.endsWith("%")) {
                            mCue.mSnapToLines = false;
                            mCue.mLinePosition = parseIntPercentage(value);
                        } else if (value.matches(".*[^0-9].*")) {
                            log_warning("cue setting", name,
                                    "contains an invalid character", value);
                        } else {
                            mCue.mSnapToLines = true;
                            mCue.mLinePosition = Integer.parseInt(value);
                        }
                    } catch (NumberFormatException e) {
                        log_warning("cue setting", name,
                                "is not numeric or percentage", value);
                    }
                    // TODO: add support for optional alignment value [,start|middle|end]
                } else if (name.equals("position")) {
                    try {
                        mCue.mTextPosition = parseIntPercentage(value);
                    } catch (NumberFormatException e) {
                        log_warning("cue setting", name,
                               "is not numeric or percentage", value);
                    }
                } else if (name.equals("size")) {
                    try {
                        mCue.mSize = parseIntPercentage(value);
                    } catch (NumberFormatException e) {
                        log_warning("cue setting", name,
                               "is not numeric or percentage", value);
                    }
                } else if (name.equals("align")) {
                    if (value.equals("start")) {
                        mCue.mAlignment = TextTrackCue.ALIGNMENT_START;
                    } else if (value.equals("middle")) {
                        mCue.mAlignment = TextTrackCue.ALIGNMENT_MIDDLE;
                    } else if (value.equals("end")) {
                        mCue.mAlignment = TextTrackCue.ALIGNMENT_END;
                    } else if (value.equals("left")) {
                        mCue.mAlignment = TextTrackCue.ALIGNMENT_LEFT;
                    } else if (value.equals("right")) {
                        mCue.mAlignment = TextTrackCue.ALIGNMENT_RIGHT;
                    } else {
                        log_warning("cue setting", name, "has invalid value", value);
                        continue;
                    }
                }
            }

            if (mCue.mLinePosition != null ||
                    mCue.mSize != 100 ||
                    (mCue.mWritingDirection !=
                        TextTrackCue.WRITING_DIRECTION_HORIZONTAL)) {
                mCue.mRegionId = "";
            }

            mPhase = mParseCueText;
        }
    };

    /* also used for notes */
    final private Phase mParseCueText = new Phase() {
        @Override
        public void parse(String line) {
            if (line.length() == 0) {
                yieldCue();
                mPhase = mParseCueId;
                return;
            } else if (mCue != null) {
                mCueTexts.add(line);
            }
        }
    };

    private void log_warning(
            String nameType, String name, String message,
            String subMessage, String value) {
        Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
                message + " ('" + value + "' " + subMessage + ")");
    }

    private void log_warning(
            String nameType, String name, String message, String value) {
        Log.w(this.getClass().getName(), nameType + " '" + name + "' " +
                message + " ('" + value + "')");
    }

    private void log_warning(String message, String value) {
        Log.w(this.getClass().getName(), message + " ('" + value + "')");
    }
}

/** @hide */
interface WebVttCueListener {
    void onCueParsed(TextTrackCue cue);
    void onRegionParsed(TextTrackRegion region);
}

/** @hide */
class WebVttTrack extends SubtitleTrack implements WebVttCueListener {
    private static final String TAG = "WebVttTrack";

    private final WebVttParser mParser = new WebVttParser(this);
    private final UnstyledTextExtractor mExtractor =
        new UnstyledTextExtractor();
    private final Tokenizer mTokenizer = new Tokenizer(mExtractor);
    private final Vector<Long> mTimestamps = new Vector<Long>();
    private final WebVttRenderingWidget mRenderingWidget;

    private final Map<String, TextTrackRegion> mRegions =
        new HashMap<String, TextTrackRegion>();
    private Long mCurrentRunID;

    WebVttTrack(WebVttRenderingWidget renderingWidget, MediaFormat format) {
        super(format);

        mRenderingWidget = renderingWidget;
    }

    @Override
    public WebVttRenderingWidget getRenderingWidget() {
        return mRenderingWidget;
    }

    @Override
    public void onData(byte[] data, boolean eos, long runID) {
        try {
            String str = new String(data, "UTF-8");

            // implement intermixing restriction for WebVTT only for now
            synchronized(mParser) {
                if (mCurrentRunID != null && runID != mCurrentRunID) {
                    throw new IllegalStateException(
                            "Run #" + mCurrentRunID +
                            " in progress.  Cannot process run #" + runID);
                }
                mCurrentRunID = runID;
                mParser.parse(str);
                if (eos) {
                    finishedRun(runID);
                    mParser.eos();
                    mRegions.clear();
                    mCurrentRunID = null;
                }
            }
        } catch (java.io.UnsupportedEncodingException e) {
            Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
        }
    }

    @Override
    public void onCueParsed(TextTrackCue cue) {
        synchronized (mParser) {
            // resolve region
            if (cue.mRegionId.length() != 0) {
                cue.mRegion = mRegions.get(cue.mRegionId);
            }

            if (DEBUG) Log.v(TAG, "adding cue " + cue);

            // tokenize text track string-lines into lines of spans
            mTokenizer.reset();
            for (String s: cue.mStrings) {
                mTokenizer.tokenize(s);
            }
            cue.mLines = mExtractor.getText();
            if (DEBUG) Log.v(TAG, cue.appendLinesToBuilder(
                    cue.appendStringsToBuilder(
                        new StringBuilder()).append(" simplified to: "))
                            .toString());

            // extract inner timestamps
            for (TextTrackCueSpan[] line: cue.mLines) {
                for (TextTrackCueSpan span: line) {
                    if (span.mTimestampMs > cue.mStartTimeMs &&
                            span.mTimestampMs < cue.mEndTimeMs &&
                            !mTimestamps.contains(span.mTimestampMs)) {
                        mTimestamps.add(span.mTimestampMs);
                    }
                }
            }

            if (mTimestamps.size() > 0) {
                cue.mInnerTimesMs = new long[mTimestamps.size()];
                for (int ix=0; ix < mTimestamps.size(); ++ix) {
                    cue.mInnerTimesMs[ix] = mTimestamps.get(ix);
                }
                mTimestamps.clear();
            } else {
                cue.mInnerTimesMs = null;
            }

            cue.mRunID = mCurrentRunID;
        }

        addCue(cue);
    }

    @Override
    public void onRegionParsed(TextTrackRegion region) {
        synchronized(mParser) {
            mRegions.put(region.mId, region);
        }
    }

    @Override
    public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
        if (!mVisible) {
            // don't keep the state if we are not visible
            return;
        }

        if (DEBUG && mTimeProvider != null) {
            try {
                Log.d(TAG, "at " +
                        (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
                        " ms the active cues are:");
            } catch (IllegalStateException e) {
                Log.d(TAG, "at (illegal state) the active cues are:");
            }
        }

        if (mRenderingWidget != null) {
            mRenderingWidget.setActiveCues(activeCues);
        }
    }
}

/**
 * Widget capable of rendering WebVTT captions.
 *
 * @hide
 */
class WebVttRenderingWidget extends ViewGroup implements SubtitleTrack.RenderingWidget {
    private static final boolean DEBUG = false;

    private static final CaptionStyle DEFAULT_CAPTION_STYLE = CaptionStyle.DEFAULT;

    private static final int DEBUG_REGION_BACKGROUND = 0x800000FF;
    private static final int DEBUG_CUE_BACKGROUND = 0x80FF0000;

    /** WebVtt specifies line height as 5.3% of the viewport height. */
    private static final float LINE_HEIGHT_RATIO = 0.0533f;

    /** Map of active regions, used to determine enter/exit. */
    private final ArrayMap<TextTrackRegion, RegionLayout> mRegionBoxes =
            new ArrayMap<TextTrackRegion, RegionLayout>();

    /** Map of active cues, used to determine enter/exit. */
    private final ArrayMap<TextTrackCue, CueLayout> mCueBoxes =
            new ArrayMap<TextTrackCue, CueLayout>();

    /** Captioning manager, used to obtain and track caption properties. */
    private final CaptioningManager mManager;

    /** Callback for rendering changes. */
    private OnChangedListener mListener;

    /** Current caption style. */
    private CaptionStyle mCaptionStyle;

    /** Current font size, computed from font scaling factor and height. */
    private float mFontSize;

    /** Whether a caption style change listener is registered. */
    private boolean mHasChangeListener;

    public WebVttRenderingWidget(Context context) {
        this(context, null);
    }

    public WebVttRenderingWidget(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public WebVttRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public WebVttRenderingWidget(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        // Cannot render text over video when layer type is hardware.
        setLayerType(View.LAYER_TYPE_SOFTWARE, null);

        mManager = (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
        mCaptionStyle = mManager.getUserStyle();
        mFontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
    }

    @Override
    public void setSize(int width, int height) {
        final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
        final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);

        measure(widthSpec, heightSpec);
        layout(0, 0, width, height);
    }

    @Override
    public void onAttachedToWindow() {
        super.onAttachedToWindow();

        manageChangeListener();
    }

    @Override
    public void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        manageChangeListener();
    }

    @Override
    public void setOnChangedListener(OnChangedListener listener) {
        mListener = listener;
    }

    @Override
    public void setVisible(boolean visible) {
        if (visible) {
            setVisibility(View.VISIBLE);
        } else {
            setVisibility(View.GONE);
        }

        manageChangeListener();
    }

    /**
     * Manages whether this renderer is listening for caption style changes.
     */
    private void manageChangeListener() {
        final boolean needsListener = isAttachedToWindow() && getVisibility() == View.VISIBLE;
        if (mHasChangeListener != needsListener) {
            mHasChangeListener = needsListener;

            if (needsListener) {
                mManager.addCaptioningChangeListener(mCaptioningListener);

                final CaptionStyle captionStyle = mManager.getUserStyle();
                final float fontSize = mManager.getFontScale() * getHeight() * LINE_HEIGHT_RATIO;
                setCaptionStyle(captionStyle, fontSize);
            } else {
                mManager.removeCaptioningChangeListener(mCaptioningListener);
            }
        }
    }

    public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
        final Context context = getContext();
        final CaptionStyle captionStyle = mCaptionStyle;
        final float fontSize = mFontSize;

        prepForPrune();

        // Ensure we have all necessary cue and region boxes.
        final int count = activeCues.size();
        for (int i = 0; i < count; i++) {
            final TextTrackCue cue = (TextTrackCue) activeCues.get(i);
            final TextTrackRegion region = cue.mRegion;
            if (region != null) {
                RegionLayout regionBox = mRegionBoxes.get(region);
                if (regionBox == null) {
                    regionBox = new RegionLayout(context, region, captionStyle, fontSize);
                    mRegionBoxes.put(region, regionBox);
                    addView(regionBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                }
                regionBox.put(cue);
            } else {
                CueLayout cueBox = mCueBoxes.get(cue);
                if (cueBox == null) {
                    cueBox = new CueLayout(context, cue, captionStyle, fontSize);
                    mCueBoxes.put(cue, cueBox);
                    addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                }
                cueBox.update();
                cueBox.setOrder(i);
            }
        }

        prune();

        // Force measurement and layout.
        final int width = getWidth();
        final int height = getHeight();
        setSize(width, height);

        if (mListener != null) {
            mListener.onChanged(this);
        }
    }

    private void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
        captionStyle = DEFAULT_CAPTION_STYLE.applyStyle(captionStyle);
        mCaptionStyle = captionStyle;
        mFontSize = fontSize;

        final int cueCount = mCueBoxes.size();
        for (int i = 0; i < cueCount; i++) {
            final CueLayout cueBox = mCueBoxes.valueAt(i);
            cueBox.setCaptionStyle(captionStyle, fontSize);
        }

        final int regionCount = mRegionBoxes.size();
        for (int i = 0; i < regionCount; i++) {
            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
            regionBox.setCaptionStyle(captionStyle, fontSize);
        }
    }

    /**
     * Remove inactive cues and regions.
     */
    private void prune() {
        int regionCount = mRegionBoxes.size();
        for (int i = 0; i < regionCount; i++) {
            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
            if (regionBox.prune()) {
                removeView(regionBox);
                mRegionBoxes.removeAt(i);
                regionCount--;
                i--;
            }
        }

        int cueCount = mCueBoxes.size();
        for (int i = 0; i < cueCount; i++) {
            final CueLayout cueBox = mCueBoxes.valueAt(i);
            if (!cueBox.isActive()) {
                removeView(cueBox);
                mCueBoxes.removeAt(i);
                cueCount--;
                i--;
            }
        }
    }

    /**
     * Reset active cues and regions.
     */
    private void prepForPrune() {
        final int regionCount = mRegionBoxes.size();
        for (int i = 0; i < regionCount; i++) {
            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
            regionBox.prepForPrune();
        }

        final int cueCount = mCueBoxes.size();
        for (int i = 0; i < cueCount; i++) {
            final CueLayout cueBox = mCueBoxes.valueAt(i);
            cueBox.prepForPrune();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int regionCount = mRegionBoxes.size();
        for (int i = 0; i < regionCount; i++) {
            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
            regionBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
        }

        final int cueCount = mCueBoxes.size();
        for (int i = 0; i < cueCount; i++) {
            final CueLayout cueBox = mCueBoxes.valueAt(i);
            cueBox.measureForParent(widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int viewportWidth = r - l;
        final int viewportHeight = b - t;

        setCaptionStyle(mCaptionStyle,
                mManager.getFontScale() * LINE_HEIGHT_RATIO * viewportHeight);

        final int regionCount = mRegionBoxes.size();
        for (int i = 0; i < regionCount; i++) {
            final RegionLayout regionBox = mRegionBoxes.valueAt(i);
            layoutRegion(viewportWidth, viewportHeight, regionBox);
        }

        final int cueCount = mCueBoxes.size();
        for (int i = 0; i < cueCount; i++) {
            final CueLayout cueBox = mCueBoxes.valueAt(i);
            layoutCue(viewportWidth, viewportHeight, cueBox);
        }
    }

    /**
     * Lays out a region within the viewport. The region handles layout for
     * contained cues.
     */
    private void layoutRegion(
            int viewportWidth, int viewportHeight,
            RegionLayout regionBox) {
        final TextTrackRegion region = regionBox.getRegion();
        final int regionHeight = regionBox.getMeasuredHeight();
        final int regionWidth = regionBox.getMeasuredWidth();

        // TODO: Account for region anchor point.
        final float x = region.mViewportAnchorPointX;
        final float y = region.mViewportAnchorPointY;
        final int left = (int) (x * (viewportWidth - regionWidth) / 100);
        final int top = (int) (y * (viewportHeight - regionHeight) / 100);

        regionBox.layout(left, top, left + regionWidth, top + regionHeight);
    }

    /**
     * Lays out a cue within the viewport.
     */
    private void layoutCue(
            int viewportWidth, int viewportHeight, CueLayout cueBox) {
        final TextTrackCue cue = cueBox.getCue();
        final int direction = getLayoutDirection();
        final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);
        final boolean cueSnapToLines = cue.mSnapToLines;

        int size = 100 * cueBox.getMeasuredWidth() / viewportWidth;

        // Determine raw x-position.
        int xPosition;
        switch (absAlignment) {
            case TextTrackCue.ALIGNMENT_LEFT:
                xPosition = cue.mTextPosition;
                break;
            case TextTrackCue.ALIGNMENT_RIGHT:
                xPosition = cue.mTextPosition - size;
                break;
            case TextTrackCue.ALIGNMENT_MIDDLE:
            default:
                xPosition = cue.mTextPosition - size / 2;
                break;
        }

        // Adjust x-position for layout.
        if (direction == LAYOUT_DIRECTION_RTL) {
            xPosition = 100 - xPosition;
        }

        // If the text track cue snap-to-lines flag is set, adjust
        // x-position and size for padding. This is equivalent to placing the
        // cue within the title-safe area.
        if (cueSnapToLines) {
            final int paddingLeft = 100 * getPaddingLeft() / viewportWidth;
            final int paddingRight = 100 * getPaddingRight() / viewportWidth;
            if (xPosition < paddingLeft && xPosition + size > paddingLeft) {
                xPosition += paddingLeft;
                size -= paddingLeft;
            }
            final float rightEdge = 100 - paddingRight;
            if (xPosition < rightEdge && xPosition + size > rightEdge) {
                size -= paddingRight;
            }
        }

        // Compute absolute left position and width.
        final int left = xPosition * viewportWidth / 100;
        final int width = size * viewportWidth / 100;

        // Determine initial y-position.
        final int yPosition = calculateLinePosition(cueBox);

        // Compute absolute final top position and height.
        final int height = cueBox.getMeasuredHeight();
        final int top;
        if (yPosition < 0) {
            // TODO: This needs to use the actual height of prior boxes.
            top = viewportHeight + yPosition * height;
        } else {
            top = yPosition * (viewportHeight - height) / 100;
        }

        // Layout cue in final position.
        cueBox.layout(left, top, left + width, top + height);
    }

    /**
     * Calculates the line position for a cue.
     * <p>
     * If the resulting position is negative, it represents a bottom-aligned
     * position relative to the number of active cues. Otherwise, it represents
     * a percentage [0-100] of the viewport height.
     */
    private int calculateLinePosition(CueLayout cueBox) {
        final TextTrackCue cue = cueBox.getCue();
        final Integer linePosition = cue.mLinePosition;
        final boolean snapToLines = cue.mSnapToLines;
        final boolean autoPosition = (linePosition == null);

        if (!snapToLines && !autoPosition && (linePosition < 0 || linePosition > 100)) {
            // Invalid line position defaults to 100.
            return 100;
        } else if (!autoPosition) {
            // Use the valid, supplied line position.
            return linePosition;
        } else if (!snapToLines) {
            // Automatic, non-snapped line position defaults to 100.
            return 100;
        } else {
            // Automatic snapped line position uses active cue order.
            return -(cueBox.mOrder + 1);
        }
    }

    /**
     * Resolves cue alignment according to the specified layout direction.
     */
    private static int resolveCueAlignment(int layoutDirection, int alignment) {
        switch (alignment) {
            case TextTrackCue.ALIGNMENT_START:
                return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
                        TextTrackCue.ALIGNMENT_LEFT : TextTrackCue.ALIGNMENT_RIGHT;
            case TextTrackCue.ALIGNMENT_END:
                return layoutDirection == View.LAYOUT_DIRECTION_LTR ?
                        TextTrackCue.ALIGNMENT_RIGHT : TextTrackCue.ALIGNMENT_LEFT;
        }
        return alignment;
    }

    private final CaptioningChangeListener mCaptioningListener = new CaptioningChangeListener() {
        @Override
        public void onFontScaleChanged(float fontScale) {
            final float fontSize = fontScale * getHeight() * LINE_HEIGHT_RATIO;
            setCaptionStyle(mCaptionStyle, fontSize);
        }

        @Override
        public void onUserStyleChanged(CaptionStyle userStyle) {
            setCaptionStyle(userStyle, mFontSize);
        }
    };

    /**
     * A text track region represents a portion of the video viewport and
     * provides a rendering area for text track cues.
     */
    private static class RegionLayout extends LinearLayout {
        private final ArrayList<CueLayout> mRegionCueBoxes = new ArrayList<CueLayout>();
        private final TextTrackRegion mRegion;

        private CaptionStyle mCaptionStyle;
        private float mFontSize;

        public RegionLayout(Context context, TextTrackRegion region, CaptionStyle captionStyle,
                float fontSize) {
            super(context);

            mRegion = region;
            mCaptionStyle = captionStyle;
            mFontSize = fontSize;

            // TODO: Add support for vertical text
            setOrientation(VERTICAL);

            if (DEBUG) {
                setBackgroundColor(DEBUG_REGION_BACKGROUND);
            } else {
                setBackgroundColor(captionStyle.windowColor);
            }
        }

        public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
            mCaptionStyle = captionStyle;
            mFontSize = fontSize;

            final int cueCount = mRegionCueBoxes.size();
            for (int i = 0; i < cueCount; i++) {
                final CueLayout cueBox = mRegionCueBoxes.get(i);
                cueBox.setCaptionStyle(captionStyle, fontSize);
            }

            setBackgroundColor(captionStyle.windowColor);
        }

        /**
         * Performs the parent's measurement responsibilities, then
         * automatically performs its own measurement.
         */
        public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
            final TextTrackRegion region = mRegion;
            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
            final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
            final int width = (int) region.mWidth;

            // Determine the absolute maximum region size as the requested size.
            final int size = width * specWidth / 100;

            widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
            measure(widthMeasureSpec, heightMeasureSpec);
        }

        /**
         * Prepares this region for pruning by setting all tracks as inactive.
         * <p>
         * Tracks that are added or updated using {@link #put(TextTrackCue)}
         * after this calling this method will be marked as active.
         */
        public void prepForPrune() {
            final int cueCount = mRegionCueBoxes.size();
            for (int i = 0; i < cueCount; i++) {
                final CueLayout cueBox = mRegionCueBoxes.get(i);
                cueBox.prepForPrune();
            }
        }

        /**
         * Adds a {@link TextTrackCue} to this region. If the track had already
         * been added, updates its active state.
         *
         * @param cue
         */
        public void put(TextTrackCue cue) {
            final int cueCount = mRegionCueBoxes.size();
            for (int i = 0; i < cueCount; i++) {
                final CueLayout cueBox = mRegionCueBoxes.get(i);
                if (cueBox.getCue() == cue) {
                    cueBox.update();
                    return;
                }
            }

            final CueLayout cueBox = new CueLayout(getContext(), cue, mCaptionStyle, mFontSize);
            mRegionCueBoxes.add(cueBox);
            addView(cueBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);

            if (getChildCount() > mRegion.mLines) {
                removeViewAt(0);
            }
        }

        /**
         * Remove all inactive tracks from this region.
         *
         * @return true if this region is empty and should be pruned
         */
        public boolean prune() {
            int cueCount = mRegionCueBoxes.size();
            for (int i = 0; i < cueCount; i++) {
                final CueLayout cueBox = mRegionCueBoxes.get(i);
                if (!cueBox.isActive()) {
                    mRegionCueBoxes.remove(i);
                    removeView(cueBox);
                    cueCount--;
                    i--;
                }
            }

            return mRegionCueBoxes.isEmpty();
        }

        /**
         * @return the region data backing this layout
         */
        public TextTrackRegion getRegion() {
            return mRegion;
        }
    }

    /**
     * A text track cue is the unit of time-sensitive data in a text track,
     * corresponding for instance for subtitles and captions to the text that
     * appears at a particular time and disappears at another time.
     * <p>
     * A single cue may contain multiple {@link SpanLayout}s, each representing a
     * single line of text.
     */
    private static class CueLayout extends LinearLayout {
        public final TextTrackCue mCue;

        private CaptionStyle mCaptionStyle;
        private float mFontSize;

        private boolean mActive;
        private int mOrder;

        public CueLayout(
                Context context, TextTrackCue cue, CaptionStyle captionStyle, float fontSize) {
            super(context);

            mCue = cue;
            mCaptionStyle = captionStyle;
            mFontSize = fontSize;

            // TODO: Add support for vertical text.
            final boolean horizontal = cue.mWritingDirection
                    == TextTrackCue.WRITING_DIRECTION_HORIZONTAL;
            setOrientation(horizontal ? VERTICAL : HORIZONTAL);

            switch (cue.mAlignment) {
                case TextTrackCue.ALIGNMENT_END:
                    setGravity(Gravity.END);
                    break;
                case TextTrackCue.ALIGNMENT_LEFT:
                    setGravity(Gravity.LEFT);
                    break;
                case TextTrackCue.ALIGNMENT_MIDDLE:
                    setGravity(horizontal
                            ? Gravity.CENTER_HORIZONTAL : Gravity.CENTER_VERTICAL);
                    break;
                case TextTrackCue.ALIGNMENT_RIGHT:
                    setGravity(Gravity.RIGHT);
                    break;
                case TextTrackCue.ALIGNMENT_START:
                    setGravity(Gravity.START);
                    break;
            }

            if (DEBUG) {
                setBackgroundColor(DEBUG_CUE_BACKGROUND);
            }

            update();
        }

        public void setCaptionStyle(CaptionStyle style, float fontSize) {
            mCaptionStyle = style;
            mFontSize = fontSize;

            final int n = getChildCount();
            for (int i = 0; i < n; i++) {
                final View child = getChildAt(i);
                if (child instanceof SpanLayout) {
                    ((SpanLayout) child).setCaptionStyle(style, fontSize);
                }
            }
        }

        public void prepForPrune() {
            mActive = false;
        }

        public void update() {
            mActive = true;

            removeAllViews();

            final int cueAlignment = resolveCueAlignment(getLayoutDirection(), mCue.mAlignment);
            final Alignment alignment;
            switch (cueAlignment) {
                case TextTrackCue.ALIGNMENT_LEFT:
                    alignment = Alignment.ALIGN_LEFT;
                    break;
                case TextTrackCue.ALIGNMENT_RIGHT:
                    alignment = Alignment.ALIGN_RIGHT;
                    break;
                case TextTrackCue.ALIGNMENT_MIDDLE:
                default:
                    alignment = Alignment.ALIGN_CENTER;
            }

            final CaptionStyle captionStyle = mCaptionStyle;
            final float fontSize = mFontSize;
            final TextTrackCueSpan[][] lines = mCue.mLines;
            final int lineCount = lines.length;
            for (int i = 0; i < lineCount; i++) {
                final SpanLayout lineBox = new SpanLayout(getContext(), lines[i]);
                lineBox.setAlignment(alignment);
                lineBox.setCaptionStyle(captionStyle, fontSize);

                addView(lineBox, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
            }
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        }

        /**
         * Performs the parent's measurement responsibilities, then
         * automatically performs its own measurement.
         */
        public void measureForParent(int widthMeasureSpec, int heightMeasureSpec) {
            final TextTrackCue cue = mCue;
            final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
            final int specHeight = MeasureSpec.getSize(heightMeasureSpec);
            final int direction = getLayoutDirection();
            final int absAlignment = resolveCueAlignment(direction, cue.mAlignment);

            // Determine the maximum size of cue based on its starting position
            // and the direction in which it grows.
            final int maximumSize;
            switch (absAlignment) {
                case TextTrackCue.ALIGNMENT_LEFT:
                    maximumSize = 100 - cue.mTextPosition;
                    break;
                case TextTrackCue.ALIGNMENT_RIGHT:
                    maximumSize = cue.mTextPosition;
                    break;
                case TextTrackCue.ALIGNMENT_MIDDLE:
                    if (cue.mTextPosition <= 50) {
                        maximumSize = cue.mTextPosition * 2;
                    } else {
                        maximumSize = (100 - cue.mTextPosition) * 2;
                    }
                    break;
                default:
                    maximumSize = 0;
            }

            // Determine absolute maximum cue size as the smaller of the
            // requested size and the maximum theoretical size.
            final int size = Math.min(cue.mSize, maximumSize) * specWidth / 100;
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.AT_MOST);
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(specHeight, MeasureSpec.AT_MOST);
            measure(widthMeasureSpec, heightMeasureSpec);
        }

        /**
         * Sets the order of this cue in the list of active cues.
         *
         * @param order the order of this cue in the list of active cues
         */
        public void setOrder(int order) {
            mOrder = order;
        }

        /**
         * @return whether this cue is marked as active
         */
        public boolean isActive() {
            return mActive;
        }

        /**
         * @return the cue data backing this layout
         */
        public TextTrackCue getCue() {
            return mCue;
        }
    }

    /**
     * A text track line represents a single line of text within a cue.
     * <p>
     * A single line may contain multiple spans, each representing a section of
     * text that may be enabled or disabled at a particular time.
     */
    private static class SpanLayout extends SubtitleView {
        private final SpannableStringBuilder mBuilder = new SpannableStringBuilder();
        private final TextTrackCueSpan[] mSpans;

        public SpanLayout(Context context, TextTrackCueSpan[] spans) {
            super(context);

            mSpans = spans;

            update();
        }

        public void update() {
            final SpannableStringBuilder builder = mBuilder;
            final TextTrackCueSpan[] spans = mSpans;

            builder.clear();
            builder.clearSpans();

            final int spanCount = spans.length;
            for (int i = 0; i < spanCount; i++) {
                final TextTrackCueSpan span = spans[i];
                if (span.mEnabled) {
                    builder.append(spans[i].mText);
                }
            }

            setText(builder);
        }

        public void setCaptionStyle(CaptionStyle captionStyle, float fontSize) {
            setBackgroundColor(captionStyle.backgroundColor);
            setForegroundColor(captionStyle.foregroundColor);
            setEdgeColor(captionStyle.edgeColor);
            setEdgeType(captionStyle.edgeType);
            setTypeface(captionStyle.getTypeface());
            setTextSize(fontSize);
        }
    }
}