content: /*
   * Copyright (C) 2014 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.TextUtils;
  import android.util.AttributeSet;
  import android.util.Log;
  import android.view.Gravity;
  import android.view.View;
  import android.view.accessibility.CaptioningManager;
  import android.widget.LinearLayout;
  import android.widget.TextView;
  
  import java.io.IOException;
  import java.io.StringReader;
  import java.util.ArrayList;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.TreeSet;
  import java.util.Vector;
  import java.util.regex.Matcher;
  import java.util.regex.Pattern;
  
  import org.xmlpull.v1.XmlPullParser;
  import org.xmlpull.v1.XmlPullParserException;
  import org.xmlpull.v1.XmlPullParserFactory;
  
  /** @hide */
  public class TtmlRenderer extends SubtitleController.Renderer {
      private final Context mContext;
  
      private static final String MEDIA_MIMETYPE_TEXT_TTML = "application/ttml+xml";
  
      private TtmlRenderingWidget mRenderingWidget;
  
      @UnsupportedAppUsage
      public TtmlRenderer(Context context) {
          mContext = context;
      }
  
      @Override
      public boolean supports(MediaFormat format) {
          if (format.containsKey(MediaFormat.KEY_MIME)) {
              return format.getString(MediaFormat.KEY_MIME).equals(MEDIA_MIMETYPE_TEXT_TTML);
          }
          return false;
      }
  
      @Override
      public SubtitleTrack createTrack(MediaFormat format) {
          if (mRenderingWidget == null) {
              mRenderingWidget = new TtmlRenderingWidget(mContext);
          }
          return new TtmlTrack(mRenderingWidget, format);
      }
  }
  
  /**
   * A class which provides utillity methods for TTML parsing.
   *
   * @hide
   */
  final class TtmlUtils {
      public static final String TAG_TT = "tt";
      public static final String TAG_HEAD = "head";
      public static final String TAG_BODY = "body";
      public static final String TAG_DIV = "div";
      public static final String TAG_P = "p";
      public static final String TAG_SPAN = "span";
      public static final String TAG_BR = "br";
      public static final String TAG_STYLE = "style";
      public static final String TAG_STYLING = "styling";
      public static final String TAG_LAYOUT = "layout";
      public static final String TAG_REGION = "region";
      public static final String TAG_METADATA = "metadata";
      public static final String TAG_SMPTE_IMAGE = "smpte:image";
      public static final String TAG_SMPTE_DATA = "smpte:data";
      public static final String TAG_SMPTE_INFORMATION = "smpte:information";
      public static final String PCDATA = "#pcdata";
      public static final String ATTR_BEGIN = "begin";
      public static final String ATTR_DURATION = "dur";
      public static final String ATTR_END = "end";
      public static final long INVALID_TIMESTAMP = Long.MAX_VALUE;
  
      /**
       * Time expression RE according to the spec:
       * http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression
       */
      private static final Pattern CLOCK_TIME = Pattern.compile(
              "^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
              + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
  
      private static final Pattern OFFSET_TIME = Pattern.compile(
              "^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
  
      private TtmlUtils() {
      }
  
      /**
       * Parses the given time expression and returns a timestamp in millisecond.
       * <p>
       * For the format of the time expression, please refer <a href=
       * "http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
       *
       * @param time A string which includes time expression.
       * @param frameRate the framerate of the stream.
       * @param subframeRate the sub-framerate of the stream
       * @param tickRate the tick rate of the stream.
       * @return the parsed timestamp in micro-second.
       * @throws NumberFormatException if the given string does not match to the
       *             format.
       */
      public static long parseTimeExpression(String time, int frameRate, int subframeRate,
              int tickRate) throws NumberFormatException {
          Matcher matcher = CLOCK_TIME.matcher(time);
          if (matcher.matches()) {
              String hours = matcher.group(1);
              double durationSeconds = Long.parseLong(hours) * 3600;
              String minutes = matcher.group(2);
              durationSeconds += Long.parseLong(minutes) * 60;
              String seconds = matcher.group(3);
              durationSeconds += Long.parseLong(seconds);
              String fraction = matcher.group(4);
              durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
              String frames = matcher.group(5);
              durationSeconds += (frames != null) ? ((double)Long.parseLong(frames)) / frameRate : 0;
              String subframes = matcher.group(6);
              durationSeconds += (subframes != null) ? ((double)Long.parseLong(subframes))
                      / subframeRate / frameRate
                      : 0;
              return (long)(durationSeconds * 1000);
          }
          matcher = OFFSET_TIME.matcher(time);
          if (matcher.matches()) {
              String timeValue = matcher.group(1);
              double value = Double.parseDouble(timeValue);
              String unit = matcher.group(2);
              if (unit.equals("h")) {
                  value *= 3600L * 1000000L;
              } else if (unit.equals("m")) {
                  value *= 60 * 1000000;
              } else if (unit.equals("s")) {
                  value *= 1000000;
              } else if (unit.equals("ms")) {
                  value *= 1000;
              } else if (unit.equals("f")) {
                  value = value / frameRate * 1000000;
              } else if (unit.equals("t")) {
                  value = value / tickRate * 1000000;
              }
              return (long)value;
          }
          throw new NumberFormatException("Malformed time expression : " + time);
      }
  
      /**
       * Applies <a href
       * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
       * default space policy</a> to the given string.
       *
       * @param in A string to apply the policy.
       */
      public static String applyDefaultSpacePolicy(String in) {
          return applySpacePolicy(in, true);
      }
  
      /**
       * Applies the space policy to the given string. This applies <a href
       * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
       * default space policy</a> with linefeed-treatment as treat-as-space
       * or preserve.
       *
       * @param in A string to apply the policy.
       * @param treatLfAsSpace Whether convert line feeds to spaces or not.
       */
      public static String applySpacePolicy(String in, boolean treatLfAsSpace) {
          // Removes CR followed by LF. ref:
          // http://www.w3.org/TR/xml/#sec-line-ends
          String crRemoved = in.replaceAll("\r\n", "\n");
          // Apply suppress-at-line-break="auto" and
          // white-space-treatment="ignore-if-surrounding-linefeed"
          String spacesNeighboringLfRemoved = crRemoved.replaceAll(" *\n *", "\n");
          // Apply linefeed-treatment="treat-as-space"
          String lfToSpace = treatLfAsSpace ? spacesNeighboringLfRemoved.replaceAll("\n", " ")
                  : spacesNeighboringLfRemoved;
          // Apply white-space-collapse="true"
          String spacesCollapsed = lfToSpace.replaceAll("[ \t\\x0B\f\r]+", " ");
          return spacesCollapsed;
      }
  
      /**
       * Returns the timed text for the given time period.
       *
       * @param root The root node of the TTML document.
       * @param startUs The start time of the time period in microsecond.
       * @param endUs The end time of the time period in microsecond.
       */
      public static String extractText(TtmlNode root, long startUs, long endUs) {
          StringBuilder text = new StringBuilder();
          extractText(root, startUs, endUs, text, false);
          return text.toString().replaceAll("\n$", "");
      }
  
      private static void extractText(TtmlNode node, long startUs, long endUs, StringBuilder out,
              boolean inPTag) {
          if (node.mName.equals(TtmlUtils.PCDATA) && inPTag) {
              out.append(node.mText);
          } else if (node.mName.equals(TtmlUtils.TAG_BR) && inPTag) {
              out.append("\n");
          } else if (node.mName.equals(TtmlUtils.TAG_METADATA)) {
              // do nothing.
          } else if (node.isActive(startUs, endUs)) {
              boolean pTag = node.mName.equals(TtmlUtils.TAG_P);
              int length = out.length();
              for (int i = 0; i < node.mChildren.size(); ++i) {
                  extractText(node.mChildren.get(i), startUs, endUs, out, pTag || inPTag);
              }
              if (pTag && length != out.length()) {
                  out.append("\n");
              }
          }
      }
  
      /**
       * Returns a TTML fragment string for the given time period.
       *
       * @param root The root node of the TTML document.
       * @param startUs The start time of the time period in microsecond.
       * @param endUs The end time of the time period in microsecond.
       */
      public static String extractTtmlFragment(TtmlNode root, long startUs, long endUs) {
          StringBuilder fragment = new StringBuilder();
          extractTtmlFragment(root, startUs, endUs, fragment);
          return fragment.toString();
      }
  
      private static void extractTtmlFragment(TtmlNode node, long startUs, long endUs,
              StringBuilder out) {
          if (node.mName.equals(TtmlUtils.PCDATA)) {
              out.append(node.mText);
          } else if (node.mName.equals(TtmlUtils.TAG_BR)) {
              out.append("<br/>");
          } else if (node.isActive(startUs, endUs)) {
              out.append("<");
              out.append(node.mName);
              out.append(node.mAttributes);
              out.append(">");
              for (int i = 0; i < node.mChildren.size(); ++i) {
                  extractTtmlFragment(node.mChildren.get(i), startUs, endUs, out);
              }
              out.append("</");
              out.append(node.mName);
              out.append(">");
          }
      }
  }
  
  /**
   * A container class which represents a cue in TTML.
   * @hide
   */
  class TtmlCue extends SubtitleTrack.Cue {
      public String mText;
      public String mTtmlFragment;
  
      public TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment) {
          this.mStartTimeMs = startTimeMs;
          this.mEndTimeMs = endTimeMs;
          this.mText = text;
          this.mTtmlFragment = ttmlFragment;
      }
  }
  
  /**
   * A container class which represents a node in TTML.
   *
   * @hide
   */
  class TtmlNode {
      public final String mName;
      public final String mAttributes;
      public final TtmlNode mParent;
      public final String mText;
      public final List<TtmlNode> mChildren = new ArrayList<TtmlNode>();
      public final long mRunId;
      public final long mStartTimeMs;
      public final long mEndTimeMs;
  
      public TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs,
              TtmlNode parent, long runId) {
          this.mName = name;
          this.mAttributes = attributes;
          this.mText = text;
          this.mStartTimeMs = startTimeMs;
          this.mEndTimeMs = endTimeMs;
          this.mParent = parent;
          this.mRunId = runId;
      }
  
      /**
       * Check if this node is active in the given time range.
       *
       * @param startTimeMs The start time of the range to check in microsecond.
       * @param endTimeMs The end time of the range to check in microsecond.
       * @return return true if the given range overlaps the time range of this
       *         node.
       */
      public boolean isActive(long startTimeMs, long endTimeMs) {
          return this.mEndTimeMs > startTimeMs && this.mStartTimeMs < endTimeMs;
      }
  }
  
  /**
   * A simple TTML parser (http://www.w3.org/TR/ttaf1-dfxp/) which supports DFXP
   * presentation profile.
   * <p>
   * Supported features in this parser are:
   * <ul>
   * <li>content
   * <li>core
   * <li>presentation
   * <li>profile
   * <li>structure
   * <li>time-offset
   * <li>timing
   * <li>tickRate
   * <li>time-clock-with-frames
   * <li>time-clock
   * <li>time-offset-with-frames
   * <li>time-offset-with-ticks
   * </ul>
   * </p>
   *
   * @hide
   */
  class TtmlParser {
      static final String TAG = "TtmlParser";
  
      // TODO: read and apply the following attributes if specified.
      private static final int DEFAULT_FRAMERATE = 30;
      private static final int DEFAULT_SUBFRAMERATE = 1;
      private static final int DEFAULT_TICKRATE = 1;
  
      private XmlPullParser mParser;
      private final TtmlNodeListener mListener;
      private long mCurrentRunId;
  
      public TtmlParser(TtmlNodeListener listener) {
          mListener = listener;
      }
  
      /**
       * Parse TTML data. Once this is called, all the previous data are
       * reset and it starts parsing for the given text.
       *
       * @param ttmlText TTML text to parse.
       * @throws XmlPullParserException
       * @throws IOException
       */
      public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException {
          mParser = null;
          mCurrentRunId = runId;
          loadParser(ttmlText);
          parseTtml();
      }
  
      private void loadParser(String ttmlFragment) throws XmlPullParserException {
          XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
          factory.setNamespaceAware(false);
          mParser = factory.newPullParser();
          StringReader in = new StringReader(ttmlFragment);
          mParser.setInput(in);
      }
  
      private void extractAttribute(XmlPullParser parser, int i, StringBuilder out) {
          out.append(" ");
          out.append(parser.getAttributeName(i));
          out.append("=\"");
          out.append(parser.getAttributeValue(i));
          out.append("\"");
      }
  
      private void parseTtml() throws XmlPullParserException, IOException {
          LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>();
          int depthInUnsupportedTag = 0;
          boolean active = true;
          while (!isEndOfDoc()) {
              int eventType = mParser.getEventType();
              TtmlNode parent = nodeStack.peekLast();
              if (active) {
                  if (eventType == XmlPullParser.START_TAG) {
                      if (!isSupportedTag(mParser.getName())) {
                          Log.w(TAG, "Unsupported tag " + mParser.getName() + " is ignored.");
                          depthInUnsupportedTag++;
                          active = false;
                      } else {
                          TtmlNode node = parseNode(parent);
                          nodeStack.addLast(node);
                          if (parent != null) {
                              parent.mChildren.add(node);
                          }
                      }
                  } else if (eventType == XmlPullParser.TEXT) {
                      String text = TtmlUtils.applyDefaultSpacePolicy(mParser.getText());
                      if (!TextUtils.isEmpty(text)) {
                          parent.mChildren.add(new TtmlNode(
                                  TtmlUtils.PCDATA, "", text, 0, TtmlUtils.INVALID_TIMESTAMP,
                                  parent, mCurrentRunId));
  
                      }
                  } else if (eventType == XmlPullParser.END_TAG) {
                      if (mParser.getName().equals(TtmlUtils.TAG_P)) {
                          mListener.onTtmlNodeParsed(nodeStack.getLast());
                      } else if (mParser.getName().equals(TtmlUtils.TAG_TT)) {
                          mListener.onRootNodeParsed(nodeStack.getLast());
                      }
                      nodeStack.removeLast();
                  }
              } else {
                  if (eventType == XmlPullParser.START_TAG) {
                      depthInUnsupportedTag++;
                  } else if (eventType == XmlPullParser.END_TAG) {
                      depthInUnsupportedTag--;
                      if (depthInUnsupportedTag == 0) {
                          active = true;
                      }
                  }
              }
              mParser.next();
          }
      }
  
      private TtmlNode parseNode(TtmlNode parent) throws XmlPullParserException, IOException {
          int eventType = mParser.getEventType();
          if (!(eventType == XmlPullParser.START_TAG)) {
              return null;
          }
          StringBuilder attrStr = new StringBuilder();
          long start = 0;
          long end = TtmlUtils.INVALID_TIMESTAMP;
          long dur = 0;
          for (int i = 0; i < mParser.getAttributeCount(); ++i) {
              String attr = mParser.getAttributeName(i);
              String value = mParser.getAttributeValue(i);
              // TODO: check if it's safe to ignore the namespace of attributes as follows.
              attr = attr.replaceFirst("^.*:", "");
              if (attr.equals(TtmlUtils.ATTR_BEGIN)) {
                  start = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE,
                          DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
              } else if (attr.equals(TtmlUtils.ATTR_END)) {
                  end = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
                          DEFAULT_TICKRATE);
              } else if (attr.equals(TtmlUtils.ATTR_DURATION)) {
                  dur = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
                          DEFAULT_TICKRATE);
              } else {
                  extractAttribute(mParser, i, attrStr);
              }
          }
          if (parent != null) {
              start += parent.mStartTimeMs;
              if (end != TtmlUtils.INVALID_TIMESTAMP) {
                  end += parent.mStartTimeMs;
              }
          }
          if (dur > 0) {
              if (end != TtmlUtils.INVALID_TIMESTAMP) {
                  Log.e(TAG, "'dur' and 'end' attributes are defined at the same time." +
                          "'end' value is ignored.");
              }
              end = start + dur;
          }
          if (parent != null) {
              // If the end time remains unspecified, then the end point is
              // interpreted as the end point of the external time interval.
              if (end == TtmlUtils.INVALID_TIMESTAMP &&
                      parent.mEndTimeMs != TtmlUtils.INVALID_TIMESTAMP &&
                      end > parent.mEndTimeMs) {
                  end = parent.mEndTimeMs;
              }
          }
          TtmlNode node = new TtmlNode(mParser.getName(), attrStr.toString(), null, start, end,
                  parent, mCurrentRunId);
          return node;
      }
  
      private boolean isEndOfDoc() throws XmlPullParserException {
          return (mParser.getEventType() == XmlPullParser.END_DOCUMENT);
      }
  
      private static boolean isSupportedTag(String tag) {
          if (tag.equals(TtmlUtils.TAG_TT) || tag.equals(TtmlUtils.TAG_HEAD) ||
                  tag.equals(TtmlUtils.TAG_BODY) || tag.equals(TtmlUtils.TAG_DIV) ||
                  tag.equals(TtmlUtils.TAG_P) || tag.equals(TtmlUtils.TAG_SPAN) ||
                  tag.equals(TtmlUtils.TAG_BR) || tag.equals(TtmlUtils.TAG_STYLE) ||
                  tag.equals(TtmlUtils.TAG_STYLING) || tag.equals(TtmlUtils.TAG_LAYOUT) ||
                  tag.equals(TtmlUtils.TAG_REGION) || tag.equals(TtmlUtils.TAG_METADATA) ||
                  tag.equals(TtmlUtils.TAG_SMPTE_IMAGE) || tag.equals(TtmlUtils.TAG_SMPTE_DATA) ||
                  tag.equals(TtmlUtils.TAG_SMPTE_INFORMATION)) {
              return true;
          }
          return false;
      }
  }
  
  /** @hide */
  interface TtmlNodeListener {
      void onTtmlNodeParsed(TtmlNode node);
      void onRootNodeParsed(TtmlNode node);
  }
  
  /** @hide */
  class TtmlTrack extends SubtitleTrack implements TtmlNodeListener {
      private static final String TAG = "TtmlTrack";
  
      private final TtmlParser mParser = new TtmlParser(this);
      private final TtmlRenderingWidget mRenderingWidget;
      private String mParsingData;
      private Long mCurrentRunID;
  
      private final LinkedList<TtmlNode> mTtmlNodes;
      private final TreeSet<Long> mTimeEvents;
      private TtmlNode mRootNode;
  
      TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format) {
          super(format);
  
          mTtmlNodes = new LinkedList<TtmlNode>();
          mTimeEvents = new TreeSet<Long>();
          mRenderingWidget = renderingWidget;
          mParsingData = "";
      }
  
      @Override
      public TtmlRenderingWidget getRenderingWidget() {
          return mRenderingWidget;
      }
  
      @Override
      public void onData(byte[] data, boolean eos, long runID) {
          try {
              // TODO: handle UTF-8 conversion properly
              String str = new String(data, "UTF-8");
  
              // implement intermixing restriction for TTML.
              synchronized(mParser) {
                  if (mCurrentRunID != null && runID != mCurrentRunID) {
                      throw new IllegalStateException(
                              "Run #" + mCurrentRunID +
                              " in progress.  Cannot process run #" + runID);
                  }
                  mCurrentRunID = runID;
                  mParsingData += str;
                  if (eos) {
                      try {
                          mParser.parse(mParsingData, mCurrentRunID);
                      } catch (XmlPullParserException e) {
                          e.printStackTrace();
                      } catch (IOException e) {
                          e.printStackTrace();
                      }
                      finishedRun(runID);
                      mParsingData = "";
                      mCurrentRunID = null;
                  }
              }
          } catch (java.io.UnsupportedEncodingException e) {
              Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
          }
      }
  
      @Override
      public void onTtmlNodeParsed(TtmlNode node) {
          mTtmlNodes.addLast(node);
          addTimeEvents(node);
      }
  
      @Override
      public void onRootNodeParsed(TtmlNode node) {
          mRootNode = node;
          TtmlCue cue = null;
          while ((cue = getNextResult()) != null) {
              addCue(cue);
          }
          mRootNode = null;
          mTtmlNodes.clear();
          mTimeEvents.clear();
      }
  
      @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:");
              }
          }
  
          mRenderingWidget.setActiveCues(activeCues);
      }
  
      /**
       * Returns a {@link TtmlCue} in the presentation time order.
       * {@code null} is returned if there is no more timed text to show.
       */
      public TtmlCue getNextResult() {
          while (mTimeEvents.size() >= 2) {
              long start = mTimeEvents.pollFirst();
              long end = mTimeEvents.first();
              List<TtmlNode> activeCues = getActiveNodes(start, end);
              if (!activeCues.isEmpty()) {
                  return new TtmlCue(start, end,
                          TtmlUtils.applySpacePolicy(TtmlUtils.extractText(
                                  mRootNode, start, end), false),
                          TtmlUtils.extractTtmlFragment(mRootNode, start, end));
              }
          }
          return null;
      }
  
      private void addTimeEvents(TtmlNode node) {
          mTimeEvents.add(node.mStartTimeMs);
          mTimeEvents.add(node.mEndTimeMs);
          for (int i = 0; i < node.mChildren.size(); ++i) {
              addTimeEvents(node.mChildren.get(i));
          }
      }
  
      private List<TtmlNode> getActiveNodes(long startTimeUs, long endTimeUs) {
          List<TtmlNode> activeNodes = new ArrayList<TtmlNode>();
          for (int i = 0; i < mTtmlNodes.size(); ++i) {
              TtmlNode node = mTtmlNodes.get(i);
              if (node.isActive(startTimeUs, endTimeUs)) {
                  activeNodes.add(node);
              }
          }
          return activeNodes;
      }
  }
  
  /**
   * Widget capable of rendering TTML captions.
   *
   * @hide
   */
  class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget {
  
      /** Callback for rendering changes. */
      private OnChangedListener mListener;
      private final TextView mTextView;
  
      public TtmlRenderingWidget(Context context) {
          this(context, null);
      }
  
      public TtmlRenderingWidget(Context context, AttributeSet attrs) {
          this(context, attrs, 0);
      }
  
      public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
          this(context, attrs, defStyleAttr, 0);
      }
  
      public TtmlRenderingWidget(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);
  
          CaptioningManager captionManager = (CaptioningManager) context.getSystemService(
                  Context.CAPTIONING_SERVICE);
          mTextView = new TextView(context);
          mTextView.setTextColor(captionManager.getUserStyle().foregroundColor);
          addView(mTextView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
          mTextView.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
      }
  
      @Override
      public void setOnChangedListener(OnChangedListener listener) {
          mListener = listener;
      }
  
      @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 setVisible(boolean visible) {
          if (visible) {
              setVisibility(View.VISIBLE);
          } else {
              setVisibility(View.GONE);
          }
      }
  
      @Override
      public void onAttachedToWindow() {
          super.onAttachedToWindow();
      }
  
      @Override
      public void onDetachedFromWindow() {
          super.onDetachedFromWindow();
      }
  
      public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
          final int count = activeCues.size();
          String subtitleText = "";
          for (int i = 0; i < count; i++) {
              TtmlCue cue = (TtmlCue) activeCues.get(i);
              subtitleText += cue.mText + "\n";
          }
          mTextView.setText(subtitleText);
  
          if (mListener != null) {
              mListener.onChanged(this);
          }
      }
  }
  