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