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