content: /*
   * 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.annotation.UnsupportedAppUsage;
  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;
  
      @UnsupportedAppUsage
      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("&amp;", "&", pos) ||
                              replaceEscape("&lt;", "<", pos) ||
                              replaceEscape("&gt;", ">", pos) ||
                              replaceEscape("&lrm;", "\u200e", pos) ||
                              replaceEscape("&rlm;", "\u200f", pos) ||
                              replaceEscape("&nbsp;", "\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 != null && mLinePosition.equals(cue.mLinePosition)) ||
                               (mLinePosition == null && cue.mLinePosition == null))) &&
                      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);
          }
      }
  }
  