1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.media;
18 
19 import android.compat.annotation.UnsupportedAppUsage;
20 import android.content.Context;
21 import android.text.TextUtils;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.view.Gravity;
25 import android.view.View;
26 import android.view.accessibility.CaptioningManager;
27 import android.widget.LinearLayout;
28 import android.widget.TextView;
29 
30 import org.xmlpull.v1.XmlPullParser;
31 import org.xmlpull.v1.XmlPullParserException;
32 import org.xmlpull.v1.XmlPullParserFactory;
33 
34 import java.io.IOException;
35 import java.io.StringReader;
36 import java.util.ArrayList;
37 import java.util.LinkedList;
38 import java.util.List;
39 import java.util.TreeSet;
40 import java.util.Vector;
41 import java.util.regex.Matcher;
42 import java.util.regex.Pattern;
43 
44 /** @hide */
45 public class TtmlRenderer extends SubtitleController.Renderer {
46     private final Context mContext;
47 
48     private static final String MEDIA_MIMETYPE_TEXT_TTML = "application/ttml+xml";
49 
50     private TtmlRenderingWidget mRenderingWidget;
51 
52     @UnsupportedAppUsage
TtmlRenderer(Context context)53     public TtmlRenderer(Context context) {
54         mContext = context;
55     }
56 
57     @Override
supports(MediaFormat format)58     public boolean supports(MediaFormat format) {
59         if (format.containsKey(MediaFormat.KEY_MIME)) {
60             return format.getString(MediaFormat.KEY_MIME).equals(MEDIA_MIMETYPE_TEXT_TTML);
61         }
62         return false;
63     }
64 
65     @Override
createTrack(MediaFormat format)66     public SubtitleTrack createTrack(MediaFormat format) {
67         if (mRenderingWidget == null) {
68             mRenderingWidget = new TtmlRenderingWidget(mContext);
69         }
70         return new TtmlTrack(mRenderingWidget, format);
71     }
72 }
73 
74 /**
75  * A class which provides utillity methods for TTML parsing.
76  *
77  * @hide
78  */
79 final class TtmlUtils {
80     public static final String TAG_TT = "tt";
81     public static final String TAG_HEAD = "head";
82     public static final String TAG_BODY = "body";
83     public static final String TAG_DIV = "div";
84     public static final String TAG_P = "p";
85     public static final String TAG_SPAN = "span";
86     public static final String TAG_BR = "br";
87     public static final String TAG_STYLE = "style";
88     public static final String TAG_STYLING = "styling";
89     public static final String TAG_LAYOUT = "layout";
90     public static final String TAG_REGION = "region";
91     public static final String TAG_METADATA = "metadata";
92     public static final String TAG_SMPTE_IMAGE = "smpte:image";
93     public static final String TAG_SMPTE_DATA = "smpte:data";
94     public static final String TAG_SMPTE_INFORMATION = "smpte:information";
95     public static final String PCDATA = "#pcdata";
96     public static final String ATTR_BEGIN = "begin";
97     public static final String ATTR_DURATION = "dur";
98     public static final String ATTR_END = "end";
99     public static final long INVALID_TIMESTAMP = Long.MAX_VALUE;
100 
101     /**
102      * Time expression RE according to the spec:
103      * http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression
104      */
105     private static final Pattern CLOCK_TIME = Pattern.compile(
106             "^([0-9][0-9]+):([0-9][0-9]):([0-9][0-9])"
107             + "(?:(\\.[0-9]+)|:([0-9][0-9])(?:\\.([0-9]+))?)?$");
108 
109     private static final Pattern OFFSET_TIME = Pattern.compile(
110             "^([0-9]+(?:\\.[0-9]+)?)(h|m|s|ms|f|t)$");
111 
TtmlUtils()112     private TtmlUtils() {
113     }
114 
115     /**
116      * Parses the given time expression and returns a timestamp in millisecond.
117      * <p>
118      * For the format of the time expression, please refer <a href=
119      * "http://www.w3.org/TR/ttaf1-dfxp/#timing-value-timeExpression">timeExpression</a>
120      *
121      * @param time A string which includes time expression.
122      * @param frameRate the framerate of the stream.
123      * @param subframeRate the sub-framerate of the stream
124      * @param tickRate the tick rate of the stream.
125      * @return the parsed timestamp in micro-second.
126      * @throws NumberFormatException if the given string does not match to the
127      *             format.
128      */
parseTimeExpression(String time, int frameRate, int subframeRate, int tickRate)129     public static long parseTimeExpression(String time, int frameRate, int subframeRate,
130             int tickRate) throws NumberFormatException {
131         Matcher matcher = CLOCK_TIME.matcher(time);
132         if (matcher.matches()) {
133             String hours = matcher.group(1);
134             double durationSeconds = Long.parseLong(hours) * 3600;
135             String minutes = matcher.group(2);
136             durationSeconds += Long.parseLong(minutes) * 60;
137             String seconds = matcher.group(3);
138             durationSeconds += Long.parseLong(seconds);
139             String fraction = matcher.group(4);
140             durationSeconds += (fraction != null) ? Double.parseDouble(fraction) : 0;
141             String frames = matcher.group(5);
142             durationSeconds += (frames != null) ? ((double)Long.parseLong(frames)) / frameRate : 0;
143             String subframes = matcher.group(6);
144             durationSeconds += (subframes != null) ? ((double)Long.parseLong(subframes))
145                     / subframeRate / frameRate
146                     : 0;
147             return (long)(durationSeconds * 1000);
148         }
149         matcher = OFFSET_TIME.matcher(time);
150         if (matcher.matches()) {
151             String timeValue = matcher.group(1);
152             double value = Double.parseDouble(timeValue);
153             String unit = matcher.group(2);
154             if (unit.equals("h")) {
155                 value *= 3600L * 1000000L;
156             } else if (unit.equals("m")) {
157                 value *= 60 * 1000000;
158             } else if (unit.equals("s")) {
159                 value *= 1000000;
160             } else if (unit.equals("ms")) {
161                 value *= 1000;
162             } else if (unit.equals("f")) {
163                 value = value / frameRate * 1000000;
164             } else if (unit.equals("t")) {
165                 value = value / tickRate * 1000000;
166             }
167             return (long)value;
168         }
169         throw new NumberFormatException("Malformed time expression : " + time);
170     }
171 
172     /**
173      * Applies <a href
174      * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
175      * default space policy</a> to the given string.
176      *
177      * @param in A string to apply the policy.
178      */
applyDefaultSpacePolicy(String in)179     public static String applyDefaultSpacePolicy(String in) {
180         return applySpacePolicy(in, true);
181     }
182 
183     /**
184      * Applies the space policy to the given string. This applies <a href
185      * src="http://www.w3.org/TR/ttaf1-dfxp/#content-attribute-space">the
186      * default space policy</a> with linefeed-treatment as treat-as-space
187      * or preserve.
188      *
189      * @param in A string to apply the policy.
190      * @param treatLfAsSpace Whether convert line feeds to spaces or not.
191      */
applySpacePolicy(String in, boolean treatLfAsSpace)192     public static String applySpacePolicy(String in, boolean treatLfAsSpace) {
193         // Removes CR followed by LF. ref:
194         // http://www.w3.org/TR/xml/#sec-line-ends
195         String crRemoved = in.replaceAll("\r\n", "\n");
196         // Apply suppress-at-line-break="auto" and
197         // white-space-treatment="ignore-if-surrounding-linefeed"
198         String spacesNeighboringLfRemoved = crRemoved.replaceAll(" *\n *", "\n");
199         // Apply linefeed-treatment="treat-as-space"
200         String lfToSpace = treatLfAsSpace ? spacesNeighboringLfRemoved.replaceAll("\n", " ")
201                 : spacesNeighboringLfRemoved;
202         // Apply white-space-collapse="true"
203         String spacesCollapsed = lfToSpace.replaceAll("[ \t\\x0B\f\r]+", " ");
204         return spacesCollapsed;
205     }
206 
207     /**
208      * Returns the timed text for the given time period.
209      *
210      * @param root The root node of the TTML document.
211      * @param startUs The start time of the time period in microsecond.
212      * @param endUs The end time of the time period in microsecond.
213      */
extractText(TtmlNode root, long startUs, long endUs)214     public static String extractText(TtmlNode root, long startUs, long endUs) {
215         StringBuilder text = new StringBuilder();
216         extractText(root, startUs, endUs, text, false);
217         return text.toString().replaceAll("\n$", "");
218     }
219 
extractText(TtmlNode node, long startUs, long endUs, StringBuilder out, boolean inPTag)220     private static void extractText(TtmlNode node, long startUs, long endUs, StringBuilder out,
221             boolean inPTag) {
222         if (node.mName.equals(TtmlUtils.PCDATA) && inPTag) {
223             out.append(node.mText);
224         } else if (node.mName.equals(TtmlUtils.TAG_BR) && inPTag) {
225             out.append("\n");
226         } else if (node.mName.equals(TtmlUtils.TAG_METADATA)) {
227             // do nothing.
228         } else if (node.isActive(startUs, endUs)) {
229             boolean pTag = node.mName.equals(TtmlUtils.TAG_P);
230             int length = out.length();
231             for (int i = 0; i < node.mChildren.size(); ++i) {
232                 extractText(node.mChildren.get(i), startUs, endUs, out, pTag || inPTag);
233             }
234             if (pTag && length != out.length()) {
235                 out.append("\n");
236             }
237         }
238     }
239 
240     /**
241      * Returns a TTML fragment string for the given time period.
242      *
243      * @param root The root node of the TTML document.
244      * @param startUs The start time of the time period in microsecond.
245      * @param endUs The end time of the time period in microsecond.
246      */
extractTtmlFragment(TtmlNode root, long startUs, long endUs)247     public static String extractTtmlFragment(TtmlNode root, long startUs, long endUs) {
248         StringBuilder fragment = new StringBuilder();
249         extractTtmlFragment(root, startUs, endUs, fragment);
250         return fragment.toString();
251     }
252 
extractTtmlFragment(TtmlNode node, long startUs, long endUs, StringBuilder out)253     private static void extractTtmlFragment(TtmlNode node, long startUs, long endUs,
254             StringBuilder out) {
255         if (node.mName.equals(TtmlUtils.PCDATA)) {
256             out.append(node.mText);
257         } else if (node.mName.equals(TtmlUtils.TAG_BR)) {
258             out.append("<br/>");
259         } else if (node.isActive(startUs, endUs)) {
260             out.append("<");
261             out.append(node.mName);
262             out.append(node.mAttributes);
263             out.append(">");
264             for (int i = 0; i < node.mChildren.size(); ++i) {
265                 extractTtmlFragment(node.mChildren.get(i), startUs, endUs, out);
266             }
267             out.append("</");
268             out.append(node.mName);
269             out.append(">");
270         }
271     }
272 }
273 
274 /**
275  * A container class which represents a cue in TTML.
276  * @hide
277  */
278 class TtmlCue extends SubtitleTrack.Cue {
279     public String mText;
280     public String mTtmlFragment;
281 
TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment)282     public TtmlCue(long startTimeMs, long endTimeMs, String text, String ttmlFragment) {
283         this.mStartTimeMs = startTimeMs;
284         this.mEndTimeMs = endTimeMs;
285         this.mText = text;
286         this.mTtmlFragment = ttmlFragment;
287     }
288 }
289 
290 /**
291  * A container class which represents a node in TTML.
292  *
293  * @hide
294  */
295 class TtmlNode {
296     public final String mName;
297     public final String mAttributes;
298     public final TtmlNode mParent;
299     public final String mText;
300     public final List<TtmlNode> mChildren = new ArrayList<TtmlNode>();
301     public final long mRunId;
302     public final long mStartTimeMs;
303     public final long mEndTimeMs;
304 
TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs, TtmlNode parent, long runId)305     public TtmlNode(String name, String attributes, String text, long startTimeMs, long endTimeMs,
306             TtmlNode parent, long runId) {
307         this.mName = name;
308         this.mAttributes = attributes;
309         this.mText = text;
310         this.mStartTimeMs = startTimeMs;
311         this.mEndTimeMs = endTimeMs;
312         this.mParent = parent;
313         this.mRunId = runId;
314     }
315 
316     /**
317      * Check if this node is active in the given time range.
318      *
319      * @param startTimeMs The start time of the range to check in microsecond.
320      * @param endTimeMs The end time of the range to check in microsecond.
321      * @return return true if the given range overlaps the time range of this
322      *         node.
323      */
isActive(long startTimeMs, long endTimeMs)324     public boolean isActive(long startTimeMs, long endTimeMs) {
325         return this.mEndTimeMs > startTimeMs && this.mStartTimeMs < endTimeMs;
326     }
327 }
328 
329 /**
330  * A simple TTML parser (http://www.w3.org/TR/ttaf1-dfxp/) which supports DFXP
331  * presentation profile.
332  * <p>
333  * Supported features in this parser are:
334  * <ul>
335  * <li>content
336  * <li>core
337  * <li>presentation
338  * <li>profile
339  * <li>structure
340  * <li>time-offset
341  * <li>timing
342  * <li>tickRate
343  * <li>time-clock-with-frames
344  * <li>time-clock
345  * <li>time-offset-with-frames
346  * <li>time-offset-with-ticks
347  * </ul>
348  * </p>
349  *
350  * @hide
351  */
352 class TtmlParser {
353     static final String TAG = "TtmlParser";
354 
355     // TODO: read and apply the following attributes if specified.
356     private static final int DEFAULT_FRAMERATE = 30;
357     private static final int DEFAULT_SUBFRAMERATE = 1;
358     private static final int DEFAULT_TICKRATE = 1;
359 
360     private XmlPullParser mParser;
361     private final TtmlNodeListener mListener;
362     private long mCurrentRunId;
363 
TtmlParser(TtmlNodeListener listener)364     public TtmlParser(TtmlNodeListener listener) {
365         mListener = listener;
366     }
367 
368     /**
369      * Parse TTML data. Once this is called, all the previous data are
370      * reset and it starts parsing for the given text.
371      *
372      * @param ttmlText TTML text to parse.
373      * @throws XmlPullParserException
374      * @throws IOException
375      */
parse(String ttmlText, long runId)376     public void parse(String ttmlText, long runId) throws XmlPullParserException, IOException {
377         mParser = null;
378         mCurrentRunId = runId;
379         loadParser(ttmlText);
380         parseTtml();
381     }
382 
loadParser(String ttmlFragment)383     private void loadParser(String ttmlFragment) throws XmlPullParserException {
384         XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
385         factory.setNamespaceAware(false);
386         mParser = factory.newPullParser();
387         StringReader in = new StringReader(ttmlFragment);
388         mParser.setInput(in);
389     }
390 
extractAttribute(XmlPullParser parser, int i, StringBuilder out)391     private void extractAttribute(XmlPullParser parser, int i, StringBuilder out) {
392         out.append(" ");
393         out.append(parser.getAttributeName(i));
394         out.append("=\"");
395         out.append(parser.getAttributeValue(i));
396         out.append("\"");
397     }
398 
parseTtml()399     private void parseTtml() throws XmlPullParserException, IOException {
400         LinkedList<TtmlNode> nodeStack = new LinkedList<TtmlNode>();
401         int depthInUnsupportedTag = 0;
402         boolean active = true;
403         while (!isEndOfDoc()) {
404             int eventType = mParser.getEventType();
405             TtmlNode parent = nodeStack.peekLast();
406             if (active) {
407                 if (eventType == XmlPullParser.START_TAG) {
408                     if (!isSupportedTag(mParser.getName())) {
409                         Log.w(TAG, "Unsupported tag " + mParser.getName() + " is ignored.");
410                         depthInUnsupportedTag++;
411                         active = false;
412                     } else {
413                         TtmlNode node = parseNode(parent);
414                         nodeStack.addLast(node);
415                         if (parent != null) {
416                             parent.mChildren.add(node);
417                         }
418                     }
419                 } else if (eventType == XmlPullParser.TEXT) {
420                     String text = TtmlUtils.applyDefaultSpacePolicy(mParser.getText());
421                     if (!TextUtils.isEmpty(text)) {
422                         parent.mChildren.add(new TtmlNode(
423                                 TtmlUtils.PCDATA, "", text, 0, TtmlUtils.INVALID_TIMESTAMP,
424                                 parent, mCurrentRunId));
425 
426                     }
427                 } else if (eventType == XmlPullParser.END_TAG) {
428                     if (mParser.getName().equals(TtmlUtils.TAG_P)) {
429                         mListener.onTtmlNodeParsed(nodeStack.getLast());
430                     } else if (mParser.getName().equals(TtmlUtils.TAG_TT)) {
431                         mListener.onRootNodeParsed(nodeStack.getLast());
432                     }
433                     nodeStack.removeLast();
434                 }
435             } else {
436                 if (eventType == XmlPullParser.START_TAG) {
437                     depthInUnsupportedTag++;
438                 } else if (eventType == XmlPullParser.END_TAG) {
439                     depthInUnsupportedTag--;
440                     if (depthInUnsupportedTag == 0) {
441                         active = true;
442                     }
443                 }
444             }
445             mParser.next();
446         }
447     }
448 
parseNode(TtmlNode parent)449     private TtmlNode parseNode(TtmlNode parent) throws XmlPullParserException, IOException {
450         int eventType = mParser.getEventType();
451         if (!(eventType == XmlPullParser.START_TAG)) {
452             return null;
453         }
454         StringBuilder attrStr = new StringBuilder();
455         long start = 0;
456         long end = TtmlUtils.INVALID_TIMESTAMP;
457         long dur = 0;
458         for (int i = 0; i < mParser.getAttributeCount(); ++i) {
459             String attr = mParser.getAttributeName(i);
460             String value = mParser.getAttributeValue(i);
461             // TODO: check if it's safe to ignore the namespace of attributes as follows.
462             attr = attr.replaceFirst("^.*:", "");
463             if (attr.equals(TtmlUtils.ATTR_BEGIN)) {
464                 start = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE,
465                         DEFAULT_SUBFRAMERATE, DEFAULT_TICKRATE);
466             } else if (attr.equals(TtmlUtils.ATTR_END)) {
467                 end = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
468                         DEFAULT_TICKRATE);
469             } else if (attr.equals(TtmlUtils.ATTR_DURATION)) {
470                 dur = TtmlUtils.parseTimeExpression(value, DEFAULT_FRAMERATE, DEFAULT_SUBFRAMERATE,
471                         DEFAULT_TICKRATE);
472             } else {
473                 extractAttribute(mParser, i, attrStr);
474             }
475         }
476         if (parent != null) {
477             start += parent.mStartTimeMs;
478             if (end != TtmlUtils.INVALID_TIMESTAMP) {
479                 end += parent.mStartTimeMs;
480             }
481         }
482         if (dur > 0) {
483             if (end != TtmlUtils.INVALID_TIMESTAMP) {
484                 Log.e(TAG, "'dur' and 'end' attributes are defined at the same time." +
485                         "'end' value is ignored.");
486             }
487             end = start + dur;
488         }
489         if (parent != null) {
490             // If the end time remains unspecified, then the end point is
491             // interpreted as the end point of the external time interval.
492             if (end == TtmlUtils.INVALID_TIMESTAMP &&
493                     parent.mEndTimeMs != TtmlUtils.INVALID_TIMESTAMP &&
494                     end > parent.mEndTimeMs) {
495                 end = parent.mEndTimeMs;
496             }
497         }
498         TtmlNode node = new TtmlNode(mParser.getName(), attrStr.toString(), null, start, end,
499                 parent, mCurrentRunId);
500         return node;
501     }
502 
isEndOfDoc()503     private boolean isEndOfDoc() throws XmlPullParserException {
504         return (mParser.getEventType() == XmlPullParser.END_DOCUMENT);
505     }
506 
isSupportedTag(String tag)507     private static boolean isSupportedTag(String tag) {
508         if (tag.equals(TtmlUtils.TAG_TT) || tag.equals(TtmlUtils.TAG_HEAD) ||
509                 tag.equals(TtmlUtils.TAG_BODY) || tag.equals(TtmlUtils.TAG_DIV) ||
510                 tag.equals(TtmlUtils.TAG_P) || tag.equals(TtmlUtils.TAG_SPAN) ||
511                 tag.equals(TtmlUtils.TAG_BR) || tag.equals(TtmlUtils.TAG_STYLE) ||
512                 tag.equals(TtmlUtils.TAG_STYLING) || tag.equals(TtmlUtils.TAG_LAYOUT) ||
513                 tag.equals(TtmlUtils.TAG_REGION) || tag.equals(TtmlUtils.TAG_METADATA) ||
514                 tag.equals(TtmlUtils.TAG_SMPTE_IMAGE) || tag.equals(TtmlUtils.TAG_SMPTE_DATA) ||
515                 tag.equals(TtmlUtils.TAG_SMPTE_INFORMATION)) {
516             return true;
517         }
518         return false;
519     }
520 }
521 
522 /** @hide */
523 interface TtmlNodeListener {
onTtmlNodeParsed(TtmlNode node)524     void onTtmlNodeParsed(TtmlNode node);
onRootNodeParsed(TtmlNode node)525     void onRootNodeParsed(TtmlNode node);
526 }
527 
528 /** @hide */
529 class TtmlTrack extends SubtitleTrack implements TtmlNodeListener {
530     private static final String TAG = "TtmlTrack";
531 
532     private final TtmlParser mParser = new TtmlParser(this);
533     private final TtmlRenderingWidget mRenderingWidget;
534     private String mParsingData;
535     private Long mCurrentRunID;
536 
537     private final LinkedList<TtmlNode> mTtmlNodes;
538     private final TreeSet<Long> mTimeEvents;
539     private TtmlNode mRootNode;
540 
TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format)541     TtmlTrack(TtmlRenderingWidget renderingWidget, MediaFormat format) {
542         super(format);
543 
544         mTtmlNodes = new LinkedList<TtmlNode>();
545         mTimeEvents = new TreeSet<Long>();
546         mRenderingWidget = renderingWidget;
547         mParsingData = "";
548     }
549 
550     @Override
getRenderingWidget()551     public TtmlRenderingWidget getRenderingWidget() {
552         return mRenderingWidget;
553     }
554 
555     @Override
onData(byte[] data, boolean eos, long runID)556     public void onData(byte[] data, boolean eos, long runID) {
557         try {
558             // TODO: handle UTF-8 conversion properly
559             String str = new String(data, "UTF-8");
560 
561             // implement intermixing restriction for TTML.
562             synchronized(mParser) {
563                 if (mCurrentRunID != null && runID != mCurrentRunID) {
564                     throw new IllegalStateException(
565                             "Run #" + mCurrentRunID +
566                             " in progress.  Cannot process run #" + runID);
567                 }
568                 mCurrentRunID = runID;
569                 mParsingData += str;
570                 if (eos) {
571                     try {
572                         mParser.parse(mParsingData, mCurrentRunID);
573                     } catch (XmlPullParserException e) {
574                         e.printStackTrace();
575                     } catch (IOException e) {
576                         e.printStackTrace();
577                     }
578                     finishedRun(runID);
579                     mParsingData = "";
580                     mCurrentRunID = null;
581                 }
582             }
583         } catch (java.io.UnsupportedEncodingException e) {
584             Log.w(TAG, "subtitle data is not UTF-8 encoded: " + e);
585         }
586     }
587 
588     @Override
onTtmlNodeParsed(TtmlNode node)589     public void onTtmlNodeParsed(TtmlNode node) {
590         mTtmlNodes.addLast(node);
591         addTimeEvents(node);
592     }
593 
594     @Override
onRootNodeParsed(TtmlNode node)595     public void onRootNodeParsed(TtmlNode node) {
596         mRootNode = node;
597         TtmlCue cue = null;
598         while ((cue = getNextResult()) != null) {
599             addCue(cue);
600         }
601         mRootNode = null;
602         mTtmlNodes.clear();
603         mTimeEvents.clear();
604     }
605 
606     @Override
updateView(Vector<SubtitleTrack.Cue> activeCues)607     public void updateView(Vector<SubtitleTrack.Cue> activeCues) {
608         if (!mVisible) {
609             // don't keep the state if we are not visible
610             return;
611         }
612 
613         if (DEBUG && mTimeProvider != null) {
614             try {
615                 Log.d(TAG, "at " +
616                         (mTimeProvider.getCurrentTimeUs(false, true) / 1000) +
617                         " ms the active cues are:");
618             } catch (IllegalStateException e) {
619                 Log.d(TAG, "at (illegal state) the active cues are:");
620             }
621         }
622 
623         mRenderingWidget.setActiveCues(activeCues);
624     }
625 
626     /**
627      * Returns a {@link TtmlCue} in the presentation time order.
628      * {@code null} is returned if there is no more timed text to show.
629      */
getNextResult()630     public TtmlCue getNextResult() {
631         while (mTimeEvents.size() >= 2) {
632             long start = mTimeEvents.pollFirst();
633             long end = mTimeEvents.first();
634             List<TtmlNode> activeCues = getActiveNodes(start, end);
635             if (!activeCues.isEmpty()) {
636                 return new TtmlCue(start, end,
637                         TtmlUtils.applySpacePolicy(TtmlUtils.extractText(
638                                 mRootNode, start, end), false),
639                         TtmlUtils.extractTtmlFragment(mRootNode, start, end));
640             }
641         }
642         return null;
643     }
644 
addTimeEvents(TtmlNode node)645     private void addTimeEvents(TtmlNode node) {
646         mTimeEvents.add(node.mStartTimeMs);
647         mTimeEvents.add(node.mEndTimeMs);
648         for (int i = 0; i < node.mChildren.size(); ++i) {
649             addTimeEvents(node.mChildren.get(i));
650         }
651     }
652 
getActiveNodes(long startTimeUs, long endTimeUs)653     private List<TtmlNode> getActiveNodes(long startTimeUs, long endTimeUs) {
654         List<TtmlNode> activeNodes = new ArrayList<TtmlNode>();
655         for (int i = 0; i < mTtmlNodes.size(); ++i) {
656             TtmlNode node = mTtmlNodes.get(i);
657             if (node.isActive(startTimeUs, endTimeUs)) {
658                 activeNodes.add(node);
659             }
660         }
661         return activeNodes;
662     }
663 }
664 
665 /**
666  * Widget capable of rendering TTML captions.
667  *
668  * @hide
669  */
670 class TtmlRenderingWidget extends LinearLayout implements SubtitleTrack.RenderingWidget {
671 
672     /** Callback for rendering changes. */
673     private OnChangedListener mListener;
674     private final TextView mTextView;
675 
TtmlRenderingWidget(Context context)676     public TtmlRenderingWidget(Context context) {
677         this(context, null);
678     }
679 
TtmlRenderingWidget(Context context, AttributeSet attrs)680     public TtmlRenderingWidget(Context context, AttributeSet attrs) {
681         this(context, attrs, 0);
682     }
683 
TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr)684     public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr) {
685         this(context, attrs, defStyleAttr, 0);
686     }
687 
TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)688     public TtmlRenderingWidget(Context context, AttributeSet attrs, int defStyleAttr,
689             int defStyleRes) {
690         super(context, attrs, defStyleAttr, defStyleRes);
691         // Cannot render text over video when layer type is hardware.
692         setLayerType(View.LAYER_TYPE_SOFTWARE, null);
693 
694         CaptioningManager captionManager = (CaptioningManager) context.getSystemService(
695                 Context.CAPTIONING_SERVICE);
696         mTextView = new TextView(context);
697         mTextView.setTextColor(captionManager.getUserStyle().foregroundColor);
698         addView(mTextView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
699         mTextView.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL);
700     }
701 
702     @Override
setOnChangedListener(OnChangedListener listener)703     public void setOnChangedListener(OnChangedListener listener) {
704         mListener = listener;
705     }
706 
707     @Override
setSize(int width, int height)708     public void setSize(int width, int height) {
709         final int widthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
710         final int heightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
711 
712         measure(widthSpec, heightSpec);
713         layout(0, 0, width, height);
714     }
715 
716     @Override
setVisible(boolean visible)717     public void setVisible(boolean visible) {
718         if (visible) {
719             setVisibility(View.VISIBLE);
720         } else {
721             setVisibility(View.GONE);
722         }
723     }
724 
725     @Override
onAttachedToWindow()726     public void onAttachedToWindow() {
727         super.onAttachedToWindow();
728     }
729 
730     @Override
onDetachedFromWindow()731     public void onDetachedFromWindow() {
732         super.onDetachedFromWindow();
733     }
734 
setActiveCues(Vector<SubtitleTrack.Cue> activeCues)735     public void setActiveCues(Vector<SubtitleTrack.Cue> activeCues) {
736         final int count = activeCues.size();
737         String subtitleText = "";
738         for (int i = 0; i < count; i++) {
739             TtmlCue cue = (TtmlCue) activeCues.get(i);
740             subtitleText += cue.mText + "\n";
741         }
742         mTextView.setText(subtitleText);
743 
744         if (mListener != null) {
745             mListener.onChanged(this);
746         }
747     }
748 }
749